From cdeab1796f3e814275d4ef7adb3d8caf3a626f96 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 29 May 2026 04:33:03 -0400 Subject: [PATCH] inti project --- .editorconfig | 18 + .env.example | 65 + .gitattributes | 11 + .gitignore | 27 + .npmrc | 2 + README.md | 289 + .../Commands/ArchitectureScanCommand.php | 51 + app/Console/Commands/DependencyMapCommand.php | 36 + .../Commands/DocumentationCoverageCommand.php | 45 + .../GenerateApiRouteInventoryCommand.php | 58 + .../LegacyUnsafeRouteAuditCommand.php | 32 + .../Commands/ReleaseFeatureFlagsCommand.php | 26 + .../ReleaseMigrationValidateCommand.php | 32 + .../Commands/ReleaseReadinessGateCommand.php | 26 + .../Commands/ReleaseShadowCompareCommand.php | 32 + .../Commands/RouteInventoryCheckCommand.php | 43 + .../IslamicSundaySchoolAttendancePolicy.php | 32 + .../IslamicSundaySchoolAttendancePolicy.php | 16 + ...amicSundaySchoolAttendanceStatusPolicy.php | 22 + .../IslamicSundaySchoolSessionPolicy.php | 13 + ...cSundaySchoolAttendanceServiceProvider.php | 23 + .../IslamicSundaySchool/Attendance/README.md | 3 + ...micSundaySchoolAttendanceReportBuilder.php | 17 + ...slamicSundaySchoolLateArrivalEvaluator.php | 16 + .../IslamicSundaySchoolSessionResolver.php | 22 + ...daySchoolCommunicationPreferencePolicy.php | 1 + ...ndaySchoolRecipientAuthorizationPolicy.php | 1 + ...ndaySchoolCommunicationServiceProvider.php | 1 + ...icSundaySchoolProgramRecipientResolver.php | 1 + .../MasjidFamilyRecipientResolver.php | 1 + .../SundayClassRecipientResolver.php | 1 + .../Resolvers/VolunteerRecipientResolver.php | 1 + .../VolunteerTeamRecipientResolver.php | 1 + .../IslamicSundaySchoolTemplateCatalog.php | 1 + .../IslamicSundaySchool/Contracts/README.md | 3 + .../IslamicSundaySchool/Curriculum/README.md | 3 + .../IslamicSundaySchool/Families/README.md | 3 + ...micSundaySchoolPaymentAllocationPolicy.php | 17 + .../IslamicSundaySchoolTuitionPolicy.php | 16 + ...amicSundaySchoolFinanceServiceProvider.php | 21 + .../IslamicSundaySchool/Finance/README.md | 3 + ...slamicSundaySchoolFinanceReportBuilder.php | 17 + .../IslamicSundaySchool/Ministry/README.md | 3 + ...micSundaySchoolPaymentAllocationPolicy.php | 16 + .../IslamicSundaySchool/Policies/README.md | 3 + .../IslamicSundaySchoolCalendarProvider.php | 27 + .../IslamicSundaySchool/Providers/README.md | 3 + .../FamilySummaryExportTemplate.php | 3 + .../QuranProgressExportTemplate.php | 3 + ...icSundaySchoolReportingServiceProvider.php | 22 + .../ReadModels/ArabicProgressReadModel.php | 4 + .../ReadModels/HalaqaReportReadModel.php | 4 + .../IslamicStudiesProgressReadModel.php | 4 + .../MasjidFamilyReportReadModel.php | 4 + .../ReadModels/QuranProgressReadModel.php | 4 + .../Reporting/Reports/ArabicLevelReport.php | 21 + .../Reports/BaseIslamicSundaySchoolReport.php | 12 + .../Reports/HalaqaAttendanceReport.php | 21 + .../Reports/IslamicStudiesProgressReport.php | 21 + .../IslamicSundaySchoolDashboardReport.php | 21 + .../Reports/MasjidFamilySummaryReport.php | 21 + .../Reporting/Reports/QuranProgressReport.php | 21 + .../Reports/SundayProgramAttendanceReport.php | 21 + .../Reporting/Reports/VolunteerTeamReport.php | 21 + .../IslamicSundaySchool/Reports/README.md | 3 + .../IslamicSundaySchoolStudentProfileData.php | 4 + .../IslamicSundaySchoolEnrollmentPolicy.php | 5 + .../IslamicSundaySchoolPromotionPolicy.php | 5 + ...SundaySchoolStudentIdentifierGenerator.php | 5 + ...amicSundaySchoolStudentServiceProvider.php | 5 + ...slamicSundaySchoolStudentReportBuilder.php | 5 + ...slamicSundaySchoolFamilyProfileService.php | 5 + ...lamicSundaySchoolStudentProfileService.php | 5 + app/Domain/README.md | 24 + .../Policies/DefaultGradeScalePolicy.php | 17 + .../Policies/DefaultPromotionPolicy.php | 14 + app/Domain/SchoolCore/Academics/README.md | 3 + .../AttendanceAuditLoggerContract.php | 13 + .../AttendanceIdempotencyServiceContract.php | 17 + .../Contracts/AttendancePolicyContract.php | 20 + .../Contracts/AttendanceServiceContract.php | 17 + .../AttendanceSessionResolverContract.php | 15 + .../AttendanceStatusPolicyContract.php | 15 + .../Contracts/BadgeLookupServiceContract.php | 14 + .../Contracts/ScannerServiceContract.php | 14 + .../StaffAttendanceServiceContract.php | 14 + .../StudentAttendanceServiceContract.php | 14 + .../Attendance/DTO/AttendanceActorData.php | 14 + .../Attendance/DTO/AttendanceAuditData.php | 24 + .../DTO/AttendanceIdempotencyData.php | 15 + .../Attendance/DTO/AttendanceRecordResult.php | 17 + .../Attendance/DTO/AttendanceSessionData.php | 19 + .../Attendance/DTO/AttendanceSessionView.php | 22 + .../Attendance/DTO/AttendanceStatusData.php | 15 + .../Attendance/DTO/AttendanceStatusResult.php | 15 + .../Attendance/DTO/AttendanceSubjectView.php | 17 + .../Attendance/DTO/AttendanceSummaryQuery.php | 16 + .../DTO/AttendanceSummaryResult.php | 13 + .../Attendance/DTO/BadgeLookupData.php | 15 + .../Attendance/DTO/BadgeLookupResult.php | 14 + .../Attendance/DTO/ProcessScanData.php | 22 + .../DTO/RecordStaffAttendanceData.php | 24 + .../DTO/RecordStudentAttendanceData.php | 24 + .../SchoolCore/Attendance/DTO/ScanResult.php | 41 + .../Attendance/Data/AttendanceMarkData.php | 12 + .../Attendance/Data/AttendanceResult.php | 11 + .../Attendance/Data/AttendanceScanData.php | 12 + .../Enums/AttendanceAuditAction.php | 18 + .../Enums/AttendanceSessionStatus.php | 12 + .../Attendance/Enums/AttendanceStatus.php | 18 + .../Enums/AttendanceSubjectType.php | 12 + .../Attendance/Enums/ScanAction.php | 11 + .../Attendance/Enums/ScanSource.php | 12 + .../AttendanceConcurrencyException.php | 9 + .../Exceptions/AttendanceException.php | 9 + .../AttendanceSessionNotFoundException.php | 9 + .../Exceptions/BadgeNotFoundException.php | 9 + .../Exceptions/DuplicateScanException.php | 9 + .../InvalidAttendanceStatusException.php | 9 + .../UnauthorizedAttendanceAccessException.php | 9 + .../Policies/DefaultAttendancePolicy.php | 33 + .../DefaultAttendanceSessionPolicy.php | 13 + .../DefaultAttendanceStatusPolicy.php | 22 + .../StandardSchoolAttendancePolicy.php | 32 + .../StandardAcademicCalendarProvider.php | 27 + app/Domain/SchoolCore/Attendance/README.md | 12 + .../Repositories/AttendanceReadRepository.php | 32 + .../Repositories/AttendanceRepository.php | 45 + .../AttendanceSessionRepository.php | 22 + .../Repositories/BadgeRepository.php | 37 + .../Services/AttendanceAuditLogger.php | 39 + .../Services/AttendanceIdempotencyService.php | 59 + .../Attendance/Services/AttendanceService.php | 44 + .../Services/AttendanceSessionResolver.php | 40 + .../Services/AttendanceStatusResolver.php | 21 + .../Services/BadgeLookupService.php | 21 + .../Services/LateArrivalEvaluator.php | 16 + .../Attendance/Services/ScannerService.php | 121 + .../Services/StaffAttendanceService.php | 57 + .../Services/StudentAttendanceService.php | 57 + .../Attendance/Support/AttendanceClock.php | 17 + .../Support/AttendanceIdempotencyKey.php | 15 + .../Support/AttendanceTransactionRunner.php | 15 + .../Attendance/Support/ScanFingerprint.php | 39 + app/Domain/SchoolCore/Authorization/README.md | 3 + .../Communication/Channels/EmailChannel.php | 1 + .../Channels/NotificationChannel.php | 1 + .../Communication/Channels/SmsChannel.php | 1 + .../Channels/WhatsAppChannel.php | 1 + .../Contracts/ChannelRouterContract.php | 4 + .../CommunicationAuditLoggerContract.php | 4 + ...ommunicationIdempotencyServiceContract.php | 4 + .../CommunicationPreferencePolicyContract.php | 4 + .../CommunicationServiceContract.php | 5 + .../DeliveryStatusServiceContract.php | 4 + .../Contracts/EmailChannelContract.php | 4 + .../MessageTemplateServiceContract.php | 4 + .../Contracts/NotificationChannelContract.php | 4 + .../RecipientDedupeServiceContract.php | 4 + .../RecipientPreviewServiceContract.php | 4 + .../Contracts/RecipientResolverContract.php | 4 + .../Contracts/SmsChannelContract.php | 4 + .../Contracts/WhatsAppChannelContract.php | 4 + .../Communication/DTO/BulkSendMessageData.php | 1 + .../Communication/DTO/BulkSendResult.php | 1 + .../Communication/DTO/ChannelMessageData.php | 1 + .../DTO/CommunicationAuditData.php | 1 + .../DTO/CommunicationPreferenceData.php | 1 + .../Communication/DTO/ComposeMessageData.php | 1 + .../Communication/DTO/DeliveryReceiptData.php | 1 + .../Communication/DTO/DeliveryResult.php | 1 + .../Communication/DTO/MessageTemplateData.php | 1 + .../Communication/DTO/RecipientData.php | 7 + .../DTO/RecipientPreviewData.php | 1 + .../DTO/RecipientPreviewResult.php | 1 + .../Communication/DTO/RecipientQueryData.php | 4 + .../DTO/ScheduledMessageResult.php | 1 + .../Communication/DTO/SendMessageData.php | 1 + .../Communication/DTO/SendResult.php | 1 + .../Enums/CommunicationAuditAction.php | 15 + .../Enums/CommunicationChannel.php | 8 + .../Enums/CommunicationStatus.php | 12 + .../Enums/DeliveryFailureReason.php | 11 + .../Enums/MessageAudienceType.php | 10 + .../Communication/Enums/MessagePriority.php | 8 + .../Communication/Enums/RecipientType.php | 9 + .../Exceptions/ChannelDeliveryException.php | 3 + .../Exceptions/CommunicationException.php | 3 + .../Exceptions/DuplicateSendException.php | 3 + ...issingCommunicationPreferenceException.php | 3 + .../RecipientPreviewRequiredException.php | 3 + .../RecipientResolutionException.php | 3 + ...authorizedCommunicationAccessException.php | 3 + .../DefaultCommunicationPreferencePolicy.php | 4 + .../DefaultRecipientAuthorizationPolicy.php | 1 + .../CommunicationLogRepository.php | 3 + .../MessageTemplateRepository.php | 3 + .../Repositories/RecipientReadRepository.php | 3 + .../Resolvers/ClassRecipientResolver.php | 4 + .../Resolvers/GuardianRecipientResolver.php | 4 + .../Resolvers/SchoolWideRecipientResolver.php | 4 + .../Resolvers/StaffRecipientResolver.php | 4 + .../Resolvers/StudentRecipientResolver.php | 4 + .../Communication/Services/ChannelRouter.php | 1 + .../Services/CommunicationAuditLogger.php | 1 + .../CommunicationIdempotencyService.php | 1 + .../Services/CommunicationService.php | 1 + .../Services/DeliveryStatusService.php | 1 + .../Services/MessageTemplateService.php | 4 + .../Services/RecipientDedupeService.php | 4 + .../Services/RecipientPreviewService.php | 4 + .../CommunicationTransactionRunner.php | 3 + .../Support/DeliveryRetryPolicy.php | 3 + .../Support/MessageSanitizer.php | 3 + .../Support/RecipientFingerprint.php | 3 + .../Communication/Support/RecipientSet.php | 3 + .../Support/TemplateRenderer.php | 3 + .../SchoolCore/Context/ContextPayload.php | 18 + .../SchoolCore/Context/SchoolContext.php | 75 + .../Context/SchoolContextException.php | 35 + .../Context/SchoolContextFactory.php | 42 + .../Context/SchoolContextResolver.php | 148 + .../SchoolCore/Context/SchoolContextStore.php | 29 + .../Context/SchoolContextValidator.php | 93 + .../AcademicCalendarProviderContract.php | 17 + .../Contracts/AttendancePolicyContract.php | 18 + .../Contracts/AttendanceServiceContract.php | 19 + .../CommunicationPreferencePolicyContract.php | 12 + .../CommunicationServiceContract.php | 18 + .../Contracts/EnrollmentServiceContract.php | 15 + .../Contracts/FileAccessPolicyContract.php | 13 + .../Contracts/FileStorageServiceContract.php | 15 + .../Contracts/GradeScalePolicyContract.php | 13 + .../Contracts/GuardianServiceContract.php | 17 + .../Contracts/InvoiceServiceContract.php | 16 + .../PaymentAllocationPolicyContract.php | 13 + .../Contracts/PaymentServiceContract.php | 17 + .../Contracts/PromotionPolicyContract.php | 15 + .../Contracts/RecipientResolverContract.php | 14 + .../Contracts/ReportServiceContract.php | 13 + .../Contracts/RequiresSchoolContext.php | 12 + .../StudentIdentifierGeneratorContract.php | 15 + .../Contracts/StudentServiceContract.php | 28 + .../Enrollment/Data/EnrollmentData.php | 11 + app/Domain/SchoolCore/Enrollment/README.md | 3 + .../Enrollment/Services/EnrollmentService.php | 14 + .../Policies/PaymentFileAccessPolicy.php | 30 + app/Domain/SchoolCore/Files/README.md | 3 + .../LaravelLocalFileStorageService.php | 45 + .../Contracts/ChargeServiceContract.php | 13 + .../Contracts/FinanceAuditLoggerContract.php | 13 + .../Contracts/FinanceFileServiceContract.php | 15 + .../Contracts/InvoiceServiceContract.php | 16 + .../PaymentAllocationPolicyContract.php | 13 + .../Contracts/PaymentServiceContract.php | 17 + .../Contracts/RefundServiceContract.php | 13 + .../Contracts/TuitionPolicyContract.php | 12 + .../Finance/DTO/CreateInvoiceData.php | 14 + .../Finance/DTO/EditPaymentData.php | 18 + .../Finance/DTO/FinanceActorData.php | 11 + .../Finance/DTO/InvoiceItemData.php | 14 + .../SchoolCore/Finance/DTO/MoneyData.php | 14 + .../Finance/DTO/PaymentFileData.php | 15 + .../Finance/DTO/RecordPaymentData.php | 18 + .../Finance/DTO/RefundPaymentData.php | 15 + .../Finance/Data/CreateInvoiceData.php | 11 + .../Finance/Data/EditPaymentData.php | 11 + .../Finance/Data/InvoiceReadModel.php | 11 + .../Finance/Data/PaymentReadModel.php | 11 + .../Finance/Data/RecordPaymentData.php | 10 + .../Finance/Enums/FinanceAuditAction.php | 21 + .../Finance/Enums/InvoiceStatus.php | 15 + .../Finance/Enums/LedgerDirection.php | 11 + .../Finance/Enums/PaymentMethod.php | 14 + .../Finance/Enums/PaymentStatus.php | 14 + .../FinanceConcurrencyException.php | 9 + .../Finance/Exceptions/FinanceException.php | 9 + .../InvalidPaymentAmountException.php | 9 + .../Exceptions/InvoiceNotFoundException.php | 9 + .../InvoiceOverpaymentException.php | 9 + .../Exceptions/PaymentNotFoundException.php | 9 + .../UnauthorizedFinanceAccessException.php | 9 + .../DefaultPaymentAllocationPolicy.php | 17 + .../Finance/Policies/DefaultTuitionPolicy.php | 16 + app/Domain/SchoolCore/Finance/README.md | 3 + .../Repositories/FinanceReadRepository.php | 16 + .../Repositories/InvoiceRepository.php | 21 + .../Repositories/PaymentRepository.php | 21 + .../Finance/Services/BalanceCalculator.php | 23 + .../Finance/Services/ChargeService.php | 22 + .../Finance/Services/FinanceAuditLogger.php | 24 + .../Finance/Services/InvoiceService.php | 43 + .../Finance/Services/PaymentFileService.php | 30 + .../Finance/Services/PaymentService.php | 48 + .../Finance/Services/RefundService.php | 25 + .../Finance/Support/FinanceIdempotencyKey.php | 14 + .../Support/FinanceTransactionRunner.php | 15 + .../SchoolCore/Finance/Support/Money.php | 40 + .../Finance/Support/PaymentFileLocator.php | 19 + .../Data/GuardianRelationshipData.php | 10 + app/Domain/SchoolCore/Guardians/README.md | 3 + .../Guardians/Services/GuardianService.php | 14 + app/Domain/SchoolCore/Identity/README.md | 3 + .../Payments/PaymentFileController.php | 44 + .../Payments/RecordPaymentCommand.php | 22 + .../Contracts/ReportAuditLoggerContract.php | 10 + .../ReportAuthorizationPolicyContract.php | 12 + .../Contracts/ReportCacheServiceContract.php | 13 + .../Contracts/ReportDefinitionContract.php | 25 + .../Contracts/ReportExportServiceContract.php | 11 + .../Contracts/ReportRegistryContract.php | 12 + .../Contracts/ReportRunnerContract.php | 11 + .../ReportSnapshotServiceContract.php | 13 + .../ScheduledReportServiceContract.php | 12 + .../Reporting/DTO/ReportAuditData.php | 7 + .../Reporting/DTO/ReportColumnData.php | 7 + .../Reporting/DTO/ReportExportData.php | 17 + .../Reporting/DTO/ReportExportResult.php | 7 + .../Reporting/DTO/ReportFilterData.php | 7 + .../Reporting/DTO/ReportRequestData.php | 19 + .../Reporting/DTO/ReportResultData.php | 18 + .../Reporting/DTO/ReportSnapshotData.php | 7 + .../Reporting/DTO/ReportSnapshotResult.php | 7 + .../Reporting/DTO/ReportSnapshotView.php | 7 + .../Reporting/DTO/ReportSortData.php | 7 + .../Reporting/DTO/ScheduledReportData.php | 7 + .../Reporting/DTO/ScheduledReportResult.php | 7 + .../Reporting/Enums/ReportAuditAction.php | 12 + .../Reporting/Enums/ReportCategory.php | 15 + .../Reporting/Enums/ReportFormat.php | 9 + .../Enums/ReportScheduleFrequency.php | 10 + .../Reporting/Enums/ReportStatus.php | 11 + .../Reporting/Enums/ReportVisibility.php | 10 + .../InvalidReportFilterException.php | 3 + .../Exceptions/ReportExportException.php | 3 + .../Exceptions/ReportNotFoundException.php | 3 + .../Exceptions/ReportTimeoutException.php | 3 + .../Exceptions/ReportingException.php | 3 + .../UnauthorizedReportAccessException.php | 3 + app/Domain/SchoolCore/Reporting/README.md | 3 + .../ReadModels/AttendanceReportReadModel.php | 17 + .../CommunicationReportReadModel.php | 17 + .../ReadModels/FinanceReportReadModel.php | 17 + .../ReadModels/StudentReportReadModel.php | 17 + .../Attendance/AttendanceSummaryReport.php | 24 + .../StaffAttendanceSummaryReport.php | 24 + .../StudentAttendanceHistoryReport.php | 24 + .../Reports/BaseReportDefinition.php | 18 + .../Communication/MessageDeliveryReport.php | 24 + .../RecipientEngagementReport.php | 24 + .../Reports/Finance/InvoiceSummaryReport.php | 24 + .../Finance/OutstandingBalanceReport.php | 24 + .../Reports/Finance/PaymentSummaryReport.php | 24 + .../Students/GuardianContactReport.php | 24 + .../Students/StudentEnrollmentReport.php | 24 + .../Reports/Students/StudentRosterReport.php | 24 + .../Repositories/ReportReadRepository.php | 3 + .../Repositories/ReportSnapshotRepository.php | 3 + .../ScheduledReportRepository.php | 3 + .../DefaultReportAuthorizationPolicy.php | 28 + .../Reporting/Services/ReportAuditLogger.php | 14 + .../Reporting/Services/ReportCacheService.php | 17 + .../Services/ReportExportService.php | 36 + .../Reporting/Services/ReportRegistry.php | 37 + .../Reporting/Services/ReportRunner.php | 33 + .../Reporting/Services/ReportService.php | 12 + .../Services/ReportSnapshotService.php | 29 + .../Services/ScheduledReportService.php | 23 + .../Support/ReportColumnFormatter.php | 14 + .../Reporting/Support/ReportContext.php | 9 + .../Support/ReportExportFilenameBuilder.php | 16 + .../Support/ReportFilterValidator.php | 18 + .../Reporting/Support/ReportQueryBuilder.php | 12 + .../Contracts/AssignmentServiceContract.php | 13 + .../Contracts/EnrollmentServiceContract.php | 14 + .../Contracts/GuardianServiceContract.php | 16 + .../Contracts/HouseholdServiceContract.php | 14 + .../Contracts/PromotionPolicyContract.php | 13 + .../Contracts/PromotionServiceContract.php | 17 + .../StudentIdentifierGeneratorContract.php | 12 + .../StudentLifecycleAuditLoggerContract.php | 10 + .../StudentProfileServiceContract.php | 12 + .../Contracts/StudentReadServiceContract.php | 13 + .../Contracts/StudentServiceContract.php | 18 + .../Students/DTO/AssignmentData.php | 8 + .../Students/DTO/AssignmentResult.php | 8 + .../Students/DTO/BulkPromotionData.php | 8 + .../Students/DTO/BulkPromotionResult.php | 8 + .../Students/DTO/CreateGuardianData.php | 8 + .../Students/DTO/CreateStudentData.php | 8 + .../Students/DTO/EnrollmentData.php | 8 + .../Students/DTO/EnrollmentResult.php | 8 + .../Students/DTO/EnrollmentView.php | 8 + .../Students/DTO/GuardianLinkResult.php | 8 + .../Students/DTO/GuardianResult.php | 8 + .../SchoolCore/Students/DTO/HouseholdData.php | 8 + .../Students/DTO/HouseholdResult.php | 8 + .../Students/DTO/LinkGuardianData.php | 8 + .../SchoolCore/Students/DTO/PromotionData.php | 8 + .../DTO/PromotionEvaluationResult.php | 8 + .../Students/DTO/PromotionResultData.php | 8 + .../Students/DTO/StudentIdentifier.php | 8 + .../Students/DTO/StudentProfileData.php | 8 + .../SchoolCore/Students/DTO/StudentResult.php | 8 + .../Students/DTO/StudentSearchQuery.php | 8 + .../DTO/StudentStatusTransitionData.php | 8 + .../SchoolCore/Students/DTO/StudentView.php | 8 + .../Students/DTO/UpdateGuardianData.php | 8 + .../Students/DTO/UpdateStudentData.php | 8 + .../Students/Data/CreateStudentData.php | 11 + .../Students/Data/StudentReadModel.php | 16 + .../Students/Data/UpdateStudentData.php | 11 + .../Students/Enums/AssignmentStatus.php | 11 + .../Students/Enums/EnrollmentStatus.php | 13 + .../Enums/GuardianRelationshipType.php | 12 + .../Students/Enums/PromotionResult.php | 12 + .../Students/Enums/StudentAuditAction.php | 14 + .../Students/Enums/StudentStatus.php | 14 + .../Exceptions/EnrollmentException.php | 7 + .../Students/Exceptions/GuardianException.php | 7 + ...nvalidStudentStatusTransitionException.php | 7 + .../Exceptions/PromotionException.php | 7 + .../Students/Exceptions/StudentException.php | 7 + .../StudentIdentifierCollisionException.php | 7 + .../Exceptions/StudentNotFoundException.php | 7 + .../UnauthorizedStudentAccessException.php | 7 + .../Policies/DefaultEnrollmentPolicy.php | 6 + .../Policies/DefaultPromotionPolicy.php | 9 + .../DefaultStudentIdentifierGenerator.php | 8 + app/Domain/SchoolCore/Students/README.md | 3 + .../Repositories/AssignmentRepository.php | 7 + .../Repositories/EnrollmentRepository.php | 7 + .../Repositories/GuardianRepository.php | 7 + .../Repositories/HouseholdRepository.php | 7 + .../Repositories/StudentReadRepository.php | 7 + .../Repositories/StudentRepository.php | 7 + .../Students/Services/AssignmentService.php | 4 + .../Students/Services/EnrollmentService.php | 4 + .../Students/Services/GuardianService.php | 4 + .../Students/Services/HouseholdService.php | 4 + .../Students/Services/PromotionService.php | 4 + .../SequentialStudentIdentifierGenerator.php | 16 + .../Services/StudentIdentifierService.php | 4 + .../Services/StudentLifecycleAuditLogger.php | 7 + .../Services/StudentProfileService.php | 4 + .../Students/Services/StudentReadService.php | 4 + .../Students/Services/StudentService.php | 24 + .../Students/StudentListQueryService.php | 35 + .../Students/StudentReadController.php | 44 + .../Support/StudentLifecycleStateMachine.php | 8 + .../Support/StudentSearchNormalizer.php | 4 + .../Support/StudentTransactionRunner.php | 5 + .../Support/AppliesSchoolContext.php | 29 + app/Domain/SchoolCore/Support/README.md | 3 + .../SchoolCore/Support/SchoolContextAware.php | 20 + app/Domain/Shared/Contracts/README.md | 3 + app/Domain/Shared/Exceptions/README.md | 3 + app/Domain/Shared/Support/README.md | 3 + app/Domain/Shared/ValueObjects/README.md | 3 + .../Api/V2/Attendance/ScanController.php | 22 + .../Controllers/Api/V2/BaseApiController.php | 29 + .../V2/Communication/BulkSendController.php | 22 + .../RecipientPreviewController.php | 22 + .../Api/V2/Finance/PaymentController.php | 35 + .../Api/V2/Finance/PaymentFileController.php | 19 + .../QuranProgressController.php | 23 + .../Api/V2/Reporting/ReportController.php | 36 + .../V2/Reporting/ReportExportController.php | 22 + .../Api/V2/Students/StudentController.php | 50 + .../Attendance/ScannerController.php | 25 + .../Attendance/StaffAttendanceController.php | 24 + .../StudentAttendanceController.php | 24 + .../Communication/BulkMessageController.php | 1 + .../MessageTemplateController.php | 1 + .../RecipientPreviewController.php | 1 + .../Finance/PaymentFileController.php | 20 + .../Reporting/ReportController.php | 28 + .../Reporting/ReportExportController.php | 18 + .../Reporting/ScheduledReportController.php | 18 + .../Controllers/StudentCreateController.php | 25 + .../Students/AssignmentController.php | 5 + .../Students/EnrollmentController.php | 5 + .../Students/GuardianController.php | 5 + ...icSundaySchoolStudentProfileController.php | 5 + .../Students/PromotionController.php | 5 + .../Students/StudentLifecycleController.php | 5 + app/Http/Middleware/DeprecationHeaders.php | 26 + app/Http/Middleware/EnsureDomainProfile.php | 26 + app/Http/Middleware/ResolveSchoolContext.php | 51 + .../Attendance/AttendanceSessionRequest.php | 24 + .../Attendance/AttendanceSummaryRequest.php | 34 + .../ManualStaffAttendanceOverrideRequest.php | 15 + ...ManualStudentAttendanceOverrideRequest.php | 15 + .../Attendance/ProcessScanRequest.php | 49 + .../RecordStaffAttendanceRequest.php | 53 + .../RecordStudentAttendanceRequest.php | 53 + .../Communication/BulkSendMessageRequest.php | 4 + .../CancelScheduledMessageRequest.php | 4 + .../CreateMessageTemplateRequest.php | 4 + .../Communication/DeliveryReceiptRequest.php | 4 + .../Communication/RecipientPreviewRequest.php | 4 + .../Communication/ScheduleMessageRequest.php | 4 + .../Communication/SendMessageRequest.php | 4 + .../UpdateCommunicationPreferenceRequest.php | 4 + .../UpdateMessageTemplateRequest.php | 4 + .../Finance/DownloadPaymentFileRequest.php | 14 + .../Requests/Finance/EditPaymentRequest.php | 18 + .../Requests/Finance/RecordPaymentRequest.php | 18 + .../Requests/Finance/RefundPaymentRequest.php | 16 + .../CancelScheduledReportRequest.php | 4 + .../Reporting/ExportReportRequest.php | 12 + .../Requests/Reporting/ListReportsRequest.php | 11 + .../Requests/Reporting/RunReportRequest.php | 12 + .../Reporting/ScheduleReportRequest.php | 12 + .../Reporting/ViewReportSnapshotRequest.php | 4 + .../Requests/Students/AssignmentRequest.php | 6 + .../Students/BulkPromotionRequest.php | 6 + .../Students/CreateGuardianRequest.php | 6 + .../Students/CreateHouseholdRequest.php | 6 + .../Students/CreateStudentRequest.php | 6 + .../Requests/Students/EnrollmentRequest.php | 6 + ...lamicSundaySchoolStudentProfileRequest.php | 5 + .../Requests/Students/LinkGuardianRequest.php | 6 + .../Requests/Students/PromotionRequest.php | 6 + .../Students/StudentSearchRequest.php | 6 + .../StudentStatusTransitionRequest.php | 6 + .../Students/UpdateStudentRequest.php | 6 + .../Attendance/AttendanceRecordResource.php | 24 + .../V2/Attendance/ScanResultResource.php | 24 + .../V2/Communication/MessageResource.php | 24 + .../RecipientPreviewResource.php | 24 + .../Resources/V2/Finance/InvoiceResource.php | 24 + .../Resources/V2/Finance/PaymentResource.php | 24 + ...amicSundaySchoolStudentProfileResource.php | 24 + .../QuranProgressReportResource.php | 24 + .../V2/Reporting/ReportExportResource.php | 24 + .../V2/Reporting/ReportResultResource.php | 24 + .../V2/Students/EnrollmentResource.php | 24 + .../V2/Students/GuardianResource.php | 24 + .../Resources/V2/Students/StudentResource.php | 24 + .../Release/RunMigrationValidationJob.php | 27 + app/Jobs/Release/RunShadowComparisonJob.php | 29 + .../IslamicSundaySchoolServiceProvider.php | 27 + .../SchoolContextServiceProvider.php | 36 + .../SchoolCoreAttendanceServiceProvider.php | 44 + ...SchoolCoreCommunicationServiceProvider.php | 9 + .../SchoolCoreFinanceServiceProvider.php | 38 + .../SchoolCoreReportingServiceProvider.php | 57 + app/Providers/SchoolCoreServiceProvider.php | 68 + .../SchoolCoreStudentServiceProvider.php | 5 + app/Support/Api/ApiExceptionRenderer.php | 47 + app/Support/Api/ApiResponse.php | 48 + app/Support/Api/ControllerClassification.php | 13 + app/Support/Api/RouteRisk.php | 11 + .../Architecture/ArchitectureFileFinder.php | 33 + app/Support/Architecture/ArchitectureRule.php | 22 + .../Architecture/ArchitectureRuleRegistry.php | 26 + .../Architecture/ArchitectureScanner.php | 59 + .../Architecture/ArchitectureViolation.php | 27 + .../Architecture/DependencyMapGenerator.php | 30 + app/Support/Release/FeatureFlagDefinition.php | 25 + app/Support/Release/FeatureFlagRegistry.php | 40 + .../Release/MigrationValidationRunner.php | 98 + app/Support/Release/ReleaseReadinessGate.php | 33 + .../Release/ShadowComparisonResult.php | 27 + .../Release/ShadowComparisonRunner.php | 28 + app/Support/Release/ValidationResult.php | 30 + app/helpers.php | 13 + artisan | 18 + bootstrap/app.php | 21 + bootstrap/cache/.gitignore | 2 + bootstrap/providers.php | 7 + composer.autoload.snippet.json | 7 + composer.json | 86 + composer.lock | 8274 +++++++++++++++++ composer.phase10.scripts.snippet.json | 9 + composer.phase9.scripts.snippet.json | 18 + config/api_controller_classification.php | 55 + config/architecture_governance.php | 120 + config/modular_release.php | 26 + config/module_ownership.php | 14 + config/release_readiness.php | 24 + config/school_context.php | 45 + ...d_unique_student_identifier_per_school.php | 24 + ..._finance_audit_and_payment_file_tables.php | 24 + ..._29_000004_add_modular_finance_indexes.php | 19 + ...9_000005_create_attendance_core_tables.php | 72 + ...tendance_scan_audit_idempotency_tables.php | 78 + ...7_create_student_lifecycle_core_tables.php | 3 + ...ent_enrollment_assignment_audit_tables.php | 3 + ...islamic_sunday_school_student_profiles.php | 3 + ...00010_create_communication_core_tables.php | 3 + ...on_delivery_template_preference_tables.php | 3 + ...29_000012_create_reporting_core_tables.php | 46 + ..._000013_create_scheduled_reports_table.php | 34 + ...te_api_route_inventory_snapshots_table.php | 23 + docs/api-deprecations.md | 18 + docs/api-route-inventory.md | 18 + docs/architecture/dependency-map.md | 20 + docs/architecture/module-boundaries.md | 19 + docs/architecture/module-ownership.md | 16 + docs/controller-classification.md | 11 + docs/deprecations.md | 7 + docs/modules/attendance.md | 14 + docs/modules/communication.md | 14 + docs/modules/finance.md | 14 + docs/modules/islamic-sunday-school.md | 14 + docs/modules/reporting.md | 14 + docs/modules/students.md | 14 + docs/openapi.phase8.yaml | 60 + docs/release-checklist.md | 14 + docs/release/api-client-migration.md | 20 + docs/release/feature-flag-rollout-map.md | 19 + docs/release/incident-response.md | 31 + ...slamic-sunday-school-production-rollout.md | 24 + docs/release/legacy-removal-plan.md | 22 + docs/release/migration-validation.md | 31 + docs/release/monitoring-dashboards.md | 40 + docs/release/performance-readiness.md | 23 + docs/release/pilot-rollout-plan.md | 26 + docs/release/post-release-review-template.md | 34 + docs/release/production-readiness-review.md | 25 + docs/release/rollback-playbooks.md | 44 + docs/release/security-readiness.md | 15 + docs/release/shadow-comparison-plan.md | 19 + docs/release/support-playbooks.md | 29 + docs/release/training-guides.md | 14 + docs/release/uat-scenarios.md | 48 + package.json | 16 + phpunit.xml | 36 + public/.htaccess | 25 + public/favicon.ico | 0 public/index.php | 20 + public/robots.txt | 2 + resources/css/app.css | 9 + resources/js/app.js | 1 + resources/views/welcome.blade.php | 223 + routes/api_phase8.php | 38 + routes/attendance_phase4.php | 15 + routes/communication_phase6.php | 3 + routes/finance_phase3.php | 9 + routes/reporting_phase7.php | 13 + routes/school_context_examples.php | 11 + routes/student_lifecycle_phase5.php | 3 + .../app/generated/api-route-inventory.json | 52 + .../generated/architecture-scan-baseline.json | 13 + storage/app/generated/dependency-map.json | 8 + .../app/generated/release-feature-flags.json | 3 + .../validation-summary.json | 4 + .../shadow-comparisons/shadow-summary.json | 4 + tests/Architecture/AttendanceBoundaryTest.php | 16 + .../CommunicationBoundaryTest.php | 16 + .../Concerns/ScansArchitecture.php | 41 + tests/Architecture/ControllerBoundaryTest.php | 21 + tests/Architecture/DeprecationPolicyTest.php | 18 + tests/Architecture/FinanceBoundaryTest.php | 21 + .../IslamicSundaySchoolBoundaryTest.php | 17 + tests/Architecture/ModuleBoundaryTest.php | 21 + ...hase10ReleaseReadinessArchitectureTest.php | 40 + .../Phase2BoundaryArchitectureTest.php | 55 + .../Phase3FinanceArchitectureTest.php | 37 + .../Phase4AttendanceArchitectureTest.php | 64 + ...Phase5StudentLifecycleArchitectureTest.php | 5 + .../Phase6CommunicationArchitectureTest.php | 1 + .../Phase7ReportingArchitectureTest.php | 41 + .../Phase8ControllerApiArchitectureTest.php | 69 + tests/Architecture/ReportingBoundaryTest.php | 21 + tests/Architecture/RouteInventoryTest.php | 21 + .../SchoolCoreArchitectureTest.php | 39 + tests/Architecture/ServiceBoundaryTest.php | 16 + tests/Architecture/StudentBoundaryTest.php | 21 + tests/Feature/Api/Phase8ApiEnvelopeTest.php | 21 + .../Release/FeatureFlagRolloutMapTest.php | 22 + .../MigrationValidationScaffoldTest.php | 22 + .../Attendance/ScannerDelegationTest.php | 17 + .../BulkSendRequiresPreviewTest.php | 1 + .../Finance/PaymentFileRouteTest.php | 17 + .../SchoolCore/PaymentFilePolicyTest.php | 28 + .../Reporting/ReportingPhase7ScaffoldTest.php | 14 + .../ResolveSchoolContextMiddlewareTest.php | 51 + .../StudentIdentifierConcurrencyTest.php | 15 + .../StudentLifecycleDelegationTest.php | 5 + ...SundaySchoolAttendanceStatusPolicyTest.php | 24 + .../AttendancePolicyTest.php | 21 + ...SundaySchoolCommunicationExtensionTest.php | 1 + ...amicSundaySchoolReportAvailabilityTest.php | 14 + ...aySchoolStudentIdentifierGeneratorTest.php | 5 + .../DefaultAttendanceStatusPolicyTest.php | 24 + .../Attendance/ScanFingerprintTest.php | 26 + .../RecipientDedupeServiceTest.php | 1 + .../SchoolCore/Context/SchoolContextTest.php | 31 + .../Context/SchoolContextValidatorTest.php | 60 + .../Finance/BalanceCalculatorTest.php | 20 + tests/Unit/SchoolCore/Finance/MoneyTest.php | 20 + .../Reporting/ReportRegistryTest.php | 14 + .../DefaultStudentIdentifierGeneratorTest.php | 5 + ...quentialStudentIdentifierGeneratorTest.php | 19 + .../StudentLifecycleStateMachineTest.php | 5 + vite.config.js | 24 + 699 files changed, 20516 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 app/Console/Commands/ArchitectureScanCommand.php create mode 100644 app/Console/Commands/DependencyMapCommand.php create mode 100644 app/Console/Commands/DocumentationCoverageCommand.php create mode 100644 app/Console/Commands/GenerateApiRouteInventoryCommand.php create mode 100644 app/Console/Commands/LegacyUnsafeRouteAuditCommand.php create mode 100644 app/Console/Commands/ReleaseFeatureFlagsCommand.php create mode 100644 app/Console/Commands/ReleaseMigrationValidateCommand.php create mode 100644 app/Console/Commands/ReleaseReadinessGateCommand.php create mode 100644 app/Console/Commands/ReleaseShadowCompareCommand.php create mode 100644 app/Console/Commands/RouteInventoryCheckCommand.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/IslamicSundaySchoolAttendancePolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolAttendancePolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolAttendanceStatusPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolSessionPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Providers/IslamicSundaySchoolAttendanceServiceProvider.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/README.md create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Reports/IslamicSundaySchoolAttendanceReportBuilder.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolLateArrivalEvaluator.php create mode 100644 app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolSessionResolver.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Policies/IslamicSundaySchoolCommunicationPreferencePolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Policies/IslamicSundaySchoolRecipientAuthorizationPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Providers/IslamicSundaySchoolCommunicationServiceProvider.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Resolvers/IslamicSundaySchoolProgramRecipientResolver.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Resolvers/MasjidFamilyRecipientResolver.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Resolvers/SundayClassRecipientResolver.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerRecipientResolver.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerTeamRecipientResolver.php create mode 100644 app/Domain/IslamicSundaySchool/Communication/Templates/IslamicSundaySchoolTemplateCatalog.php create mode 100644 app/Domain/IslamicSundaySchool/Contracts/README.md create mode 100644 app/Domain/IslamicSundaySchool/Curriculum/README.md create mode 100644 app/Domain/IslamicSundaySchool/Families/README.md create mode 100644 app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolTuitionPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Finance/Providers/IslamicSundaySchoolFinanceServiceProvider.php create mode 100644 app/Domain/IslamicSundaySchool/Finance/README.md create mode 100644 app/Domain/IslamicSundaySchool/Finance/Reports/IslamicSundaySchoolFinanceReportBuilder.php create mode 100644 app/Domain/IslamicSundaySchool/Ministry/README.md create mode 100644 app/Domain/IslamicSundaySchool/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Policies/README.md create mode 100644 app/Domain/IslamicSundaySchool/Providers/IslamicSundaySchoolCalendarProvider.php create mode 100644 app/Domain/IslamicSundaySchool/Providers/README.md create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ExportTemplates/FamilySummaryExportTemplate.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ExportTemplates/QuranProgressExportTemplate.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Providers/IslamicSundaySchoolReportingServiceProvider.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ReadModels/ArabicProgressReadModel.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ReadModels/HalaqaReportReadModel.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ReadModels/IslamicStudiesProgressReadModel.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ReadModels/MasjidFamilyReportReadModel.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/ReadModels/QuranProgressReadModel.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/ArabicLevelReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/BaseIslamicSundaySchoolReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/HalaqaAttendanceReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicStudiesProgressReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicSundaySchoolDashboardReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/MasjidFamilySummaryReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/QuranProgressReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/SundayProgramAttendanceReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reporting/Reports/VolunteerTeamReport.php create mode 100644 app/Domain/IslamicSundaySchool/Reports/README.md create mode 100644 app/Domain/IslamicSundaySchool/Students/DTO/IslamicSundaySchoolStudentProfileData.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolEnrollmentPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolPromotionPolicy.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolStudentIdentifierGenerator.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Providers/IslamicSundaySchoolStudentServiceProvider.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Reports/IslamicSundaySchoolStudentReportBuilder.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolFamilyProfileService.php create mode 100644 app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolStudentProfileService.php create mode 100644 app/Domain/README.md create mode 100644 app/Domain/SchoolCore/Academics/Policies/DefaultGradeScalePolicy.php create mode 100644 app/Domain/SchoolCore/Academics/Policies/DefaultPromotionPolicy.php create mode 100644 app/Domain/SchoolCore/Academics/README.md create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/AttendanceAuditLoggerContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/AttendanceIdempotencyServiceContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/AttendancePolicyContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/AttendanceServiceContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/AttendanceSessionResolverContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/AttendanceStatusPolicyContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/BadgeLookupServiceContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/ScannerServiceContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/StaffAttendanceServiceContract.php create mode 100644 app/Domain/SchoolCore/Attendance/Contracts/StudentAttendanceServiceContract.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceActorData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceAuditData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceIdempotencyData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceRecordResult.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceSessionData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceSessionView.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceStatusData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceStatusResult.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceSubjectView.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceSummaryQuery.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/AttendanceSummaryResult.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/BadgeLookupData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/BadgeLookupResult.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/ProcessScanData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/RecordStaffAttendanceData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/RecordStudentAttendanceData.php create mode 100644 app/Domain/SchoolCore/Attendance/DTO/ScanResult.php create mode 100644 app/Domain/SchoolCore/Attendance/Data/AttendanceMarkData.php create mode 100644 app/Domain/SchoolCore/Attendance/Data/AttendanceResult.php create mode 100644 app/Domain/SchoolCore/Attendance/Data/AttendanceScanData.php create mode 100644 app/Domain/SchoolCore/Attendance/Enums/AttendanceAuditAction.php create mode 100644 app/Domain/SchoolCore/Attendance/Enums/AttendanceSessionStatus.php create mode 100644 app/Domain/SchoolCore/Attendance/Enums/AttendanceStatus.php create mode 100644 app/Domain/SchoolCore/Attendance/Enums/AttendanceSubjectType.php create mode 100644 app/Domain/SchoolCore/Attendance/Enums/ScanAction.php create mode 100644 app/Domain/SchoolCore/Attendance/Enums/ScanSource.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/AttendanceConcurrencyException.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/AttendanceException.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/AttendanceSessionNotFoundException.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/BadgeNotFoundException.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/DuplicateScanException.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/InvalidAttendanceStatusException.php create mode 100644 app/Domain/SchoolCore/Attendance/Exceptions/UnauthorizedAttendanceAccessException.php create mode 100644 app/Domain/SchoolCore/Attendance/Policies/DefaultAttendancePolicy.php create mode 100644 app/Domain/SchoolCore/Attendance/Policies/DefaultAttendanceSessionPolicy.php create mode 100644 app/Domain/SchoolCore/Attendance/Policies/DefaultAttendanceStatusPolicy.php create mode 100644 app/Domain/SchoolCore/Attendance/Policies/StandardSchoolAttendancePolicy.php create mode 100644 app/Domain/SchoolCore/Attendance/Providers/StandardAcademicCalendarProvider.php create mode 100644 app/Domain/SchoolCore/Attendance/README.md create mode 100644 app/Domain/SchoolCore/Attendance/Repositories/AttendanceReadRepository.php create mode 100644 app/Domain/SchoolCore/Attendance/Repositories/AttendanceRepository.php create mode 100644 app/Domain/SchoolCore/Attendance/Repositories/AttendanceSessionRepository.php create mode 100644 app/Domain/SchoolCore/Attendance/Repositories/BadgeRepository.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/AttendanceAuditLogger.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/AttendanceIdempotencyService.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/AttendanceService.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/AttendanceSessionResolver.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/AttendanceStatusResolver.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/BadgeLookupService.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/LateArrivalEvaluator.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/ScannerService.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/StaffAttendanceService.php create mode 100644 app/Domain/SchoolCore/Attendance/Services/StudentAttendanceService.php create mode 100644 app/Domain/SchoolCore/Attendance/Support/AttendanceClock.php create mode 100644 app/Domain/SchoolCore/Attendance/Support/AttendanceIdempotencyKey.php create mode 100644 app/Domain/SchoolCore/Attendance/Support/AttendanceTransactionRunner.php create mode 100644 app/Domain/SchoolCore/Attendance/Support/ScanFingerprint.php create mode 100644 app/Domain/SchoolCore/Authorization/README.md create mode 100644 app/Domain/SchoolCore/Communication/Channels/EmailChannel.php create mode 100644 app/Domain/SchoolCore/Communication/Channels/NotificationChannel.php create mode 100644 app/Domain/SchoolCore/Communication/Channels/SmsChannel.php create mode 100644 app/Domain/SchoolCore/Communication/Channels/WhatsAppChannel.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/ChannelRouterContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/CommunicationAuditLoggerContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/CommunicationIdempotencyServiceContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/CommunicationPreferencePolicyContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/CommunicationServiceContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/DeliveryStatusServiceContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/EmailChannelContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/MessageTemplateServiceContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/NotificationChannelContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/RecipientDedupeServiceContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/RecipientPreviewServiceContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/RecipientResolverContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/SmsChannelContract.php create mode 100644 app/Domain/SchoolCore/Communication/Contracts/WhatsAppChannelContract.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/BulkSendMessageData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/BulkSendResult.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/ChannelMessageData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/CommunicationAuditData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/CommunicationPreferenceData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/ComposeMessageData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/DeliveryReceiptData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/DeliveryResult.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/MessageTemplateData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/RecipientData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/RecipientPreviewData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/RecipientPreviewResult.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/RecipientQueryData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/ScheduledMessageResult.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/SendMessageData.php create mode 100644 app/Domain/SchoolCore/Communication/DTO/SendResult.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/CommunicationAuditAction.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/CommunicationChannel.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/CommunicationStatus.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/DeliveryFailureReason.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/MessageAudienceType.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/MessagePriority.php create mode 100644 app/Domain/SchoolCore/Communication/Enums/RecipientType.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/ChannelDeliveryException.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/CommunicationException.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/DuplicateSendException.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/MissingCommunicationPreferenceException.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/RecipientPreviewRequiredException.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/RecipientResolutionException.php create mode 100644 app/Domain/SchoolCore/Communication/Exceptions/UnauthorizedCommunicationAccessException.php create mode 100644 app/Domain/SchoolCore/Communication/Policies/DefaultCommunicationPreferencePolicy.php create mode 100644 app/Domain/SchoolCore/Communication/Policies/DefaultRecipientAuthorizationPolicy.php create mode 100644 app/Domain/SchoolCore/Communication/Repositories/CommunicationLogRepository.php create mode 100644 app/Domain/SchoolCore/Communication/Repositories/MessageTemplateRepository.php create mode 100644 app/Domain/SchoolCore/Communication/Repositories/RecipientReadRepository.php create mode 100644 app/Domain/SchoolCore/Communication/Resolvers/ClassRecipientResolver.php create mode 100644 app/Domain/SchoolCore/Communication/Resolvers/GuardianRecipientResolver.php create mode 100644 app/Domain/SchoolCore/Communication/Resolvers/SchoolWideRecipientResolver.php create mode 100644 app/Domain/SchoolCore/Communication/Resolvers/StaffRecipientResolver.php create mode 100644 app/Domain/SchoolCore/Communication/Resolvers/StudentRecipientResolver.php create mode 100644 app/Domain/SchoolCore/Communication/Services/ChannelRouter.php create mode 100644 app/Domain/SchoolCore/Communication/Services/CommunicationAuditLogger.php create mode 100644 app/Domain/SchoolCore/Communication/Services/CommunicationIdempotencyService.php create mode 100644 app/Domain/SchoolCore/Communication/Services/CommunicationService.php create mode 100644 app/Domain/SchoolCore/Communication/Services/DeliveryStatusService.php create mode 100644 app/Domain/SchoolCore/Communication/Services/MessageTemplateService.php create mode 100644 app/Domain/SchoolCore/Communication/Services/RecipientDedupeService.php create mode 100644 app/Domain/SchoolCore/Communication/Services/RecipientPreviewService.php create mode 100644 app/Domain/SchoolCore/Communication/Support/CommunicationTransactionRunner.php create mode 100644 app/Domain/SchoolCore/Communication/Support/DeliveryRetryPolicy.php create mode 100644 app/Domain/SchoolCore/Communication/Support/MessageSanitizer.php create mode 100644 app/Domain/SchoolCore/Communication/Support/RecipientFingerprint.php create mode 100644 app/Domain/SchoolCore/Communication/Support/RecipientSet.php create mode 100644 app/Domain/SchoolCore/Communication/Support/TemplateRenderer.php create mode 100644 app/Domain/SchoolCore/Context/ContextPayload.php create mode 100644 app/Domain/SchoolCore/Context/SchoolContext.php create mode 100644 app/Domain/SchoolCore/Context/SchoolContextException.php create mode 100644 app/Domain/SchoolCore/Context/SchoolContextFactory.php create mode 100644 app/Domain/SchoolCore/Context/SchoolContextResolver.php create mode 100644 app/Domain/SchoolCore/Context/SchoolContextStore.php create mode 100644 app/Domain/SchoolCore/Context/SchoolContextValidator.php create mode 100644 app/Domain/SchoolCore/Contracts/AcademicCalendarProviderContract.php create mode 100644 app/Domain/SchoolCore/Contracts/AttendancePolicyContract.php create mode 100644 app/Domain/SchoolCore/Contracts/AttendanceServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/CommunicationPreferencePolicyContract.php create mode 100644 app/Domain/SchoolCore/Contracts/CommunicationServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/EnrollmentServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/FileAccessPolicyContract.php create mode 100644 app/Domain/SchoolCore/Contracts/FileStorageServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/GradeScalePolicyContract.php create mode 100644 app/Domain/SchoolCore/Contracts/GuardianServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/InvoiceServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/PaymentAllocationPolicyContract.php create mode 100644 app/Domain/SchoolCore/Contracts/PaymentServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/PromotionPolicyContract.php create mode 100644 app/Domain/SchoolCore/Contracts/RecipientResolverContract.php create mode 100644 app/Domain/SchoolCore/Contracts/ReportServiceContract.php create mode 100644 app/Domain/SchoolCore/Contracts/RequiresSchoolContext.php create mode 100644 app/Domain/SchoolCore/Contracts/StudentIdentifierGeneratorContract.php create mode 100644 app/Domain/SchoolCore/Contracts/StudentServiceContract.php create mode 100644 app/Domain/SchoolCore/Enrollment/Data/EnrollmentData.php create mode 100644 app/Domain/SchoolCore/Enrollment/README.md create mode 100644 app/Domain/SchoolCore/Enrollment/Services/EnrollmentService.php create mode 100644 app/Domain/SchoolCore/Files/Policies/PaymentFileAccessPolicy.php create mode 100644 app/Domain/SchoolCore/Files/README.md create mode 100644 app/Domain/SchoolCore/Files/Services/LaravelLocalFileStorageService.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/ChargeServiceContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/FinanceAuditLoggerContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/FinanceFileServiceContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/InvoiceServiceContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/PaymentAllocationPolicyContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/PaymentServiceContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/RefundServiceContract.php create mode 100644 app/Domain/SchoolCore/Finance/Contracts/TuitionPolicyContract.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/CreateInvoiceData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/EditPaymentData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/FinanceActorData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/InvoiceItemData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/MoneyData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/PaymentFileData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/RecordPaymentData.php create mode 100644 app/Domain/SchoolCore/Finance/DTO/RefundPaymentData.php create mode 100644 app/Domain/SchoolCore/Finance/Data/CreateInvoiceData.php create mode 100644 app/Domain/SchoolCore/Finance/Data/EditPaymentData.php create mode 100644 app/Domain/SchoolCore/Finance/Data/InvoiceReadModel.php create mode 100644 app/Domain/SchoolCore/Finance/Data/PaymentReadModel.php create mode 100644 app/Domain/SchoolCore/Finance/Data/RecordPaymentData.php create mode 100644 app/Domain/SchoolCore/Finance/Enums/FinanceAuditAction.php create mode 100644 app/Domain/SchoolCore/Finance/Enums/InvoiceStatus.php create mode 100644 app/Domain/SchoolCore/Finance/Enums/LedgerDirection.php create mode 100644 app/Domain/SchoolCore/Finance/Enums/PaymentMethod.php create mode 100644 app/Domain/SchoolCore/Finance/Enums/PaymentStatus.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/FinanceConcurrencyException.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/FinanceException.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/InvalidPaymentAmountException.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/InvoiceNotFoundException.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/InvoiceOverpaymentException.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/PaymentNotFoundException.php create mode 100644 app/Domain/SchoolCore/Finance/Exceptions/UnauthorizedFinanceAccessException.php create mode 100644 app/Domain/SchoolCore/Finance/Policies/DefaultPaymentAllocationPolicy.php create mode 100644 app/Domain/SchoolCore/Finance/Policies/DefaultTuitionPolicy.php create mode 100644 app/Domain/SchoolCore/Finance/README.md create mode 100644 app/Domain/SchoolCore/Finance/Repositories/FinanceReadRepository.php create mode 100644 app/Domain/SchoolCore/Finance/Repositories/InvoiceRepository.php create mode 100644 app/Domain/SchoolCore/Finance/Repositories/PaymentRepository.php create mode 100644 app/Domain/SchoolCore/Finance/Services/BalanceCalculator.php create mode 100644 app/Domain/SchoolCore/Finance/Services/ChargeService.php create mode 100644 app/Domain/SchoolCore/Finance/Services/FinanceAuditLogger.php create mode 100644 app/Domain/SchoolCore/Finance/Services/InvoiceService.php create mode 100644 app/Domain/SchoolCore/Finance/Services/PaymentFileService.php create mode 100644 app/Domain/SchoolCore/Finance/Services/PaymentService.php create mode 100644 app/Domain/SchoolCore/Finance/Services/RefundService.php create mode 100644 app/Domain/SchoolCore/Finance/Support/FinanceIdempotencyKey.php create mode 100644 app/Domain/SchoolCore/Finance/Support/FinanceTransactionRunner.php create mode 100644 app/Domain/SchoolCore/Finance/Support/Money.php create mode 100644 app/Domain/SchoolCore/Finance/Support/PaymentFileLocator.php create mode 100644 app/Domain/SchoolCore/Guardians/Data/GuardianRelationshipData.php create mode 100644 app/Domain/SchoolCore/Guardians/README.md create mode 100644 app/Domain/SchoolCore/Guardians/Services/GuardianService.php create mode 100644 app/Domain/SchoolCore/Identity/README.md create mode 100644 app/Domain/SchoolCore/Payments/PaymentFileController.php create mode 100644 app/Domain/SchoolCore/Payments/RecordPaymentCommand.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportAuditLoggerContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportAuthorizationPolicyContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportCacheServiceContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportDefinitionContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportExportServiceContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportRegistryContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportRunnerContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ReportSnapshotServiceContract.php create mode 100644 app/Domain/SchoolCore/Reporting/Contracts/ScheduledReportServiceContract.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportAuditData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportColumnData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportExportData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportExportResult.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportFilterData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportRequestData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportResultData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportSnapshotData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportSnapshotResult.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportSnapshotView.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ReportSortData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ScheduledReportData.php create mode 100644 app/Domain/SchoolCore/Reporting/DTO/ScheduledReportResult.php create mode 100644 app/Domain/SchoolCore/Reporting/Enums/ReportAuditAction.php create mode 100644 app/Domain/SchoolCore/Reporting/Enums/ReportCategory.php create mode 100644 app/Domain/SchoolCore/Reporting/Enums/ReportFormat.php create mode 100644 app/Domain/SchoolCore/Reporting/Enums/ReportScheduleFrequency.php create mode 100644 app/Domain/SchoolCore/Reporting/Enums/ReportStatus.php create mode 100644 app/Domain/SchoolCore/Reporting/Enums/ReportVisibility.php create mode 100644 app/Domain/SchoolCore/Reporting/Exceptions/InvalidReportFilterException.php create mode 100644 app/Domain/SchoolCore/Reporting/Exceptions/ReportExportException.php create mode 100644 app/Domain/SchoolCore/Reporting/Exceptions/ReportNotFoundException.php create mode 100644 app/Domain/SchoolCore/Reporting/Exceptions/ReportTimeoutException.php create mode 100644 app/Domain/SchoolCore/Reporting/Exceptions/ReportingException.php create mode 100644 app/Domain/SchoolCore/Reporting/Exceptions/UnauthorizedReportAccessException.php create mode 100644 app/Domain/SchoolCore/Reporting/README.md create mode 100644 app/Domain/SchoolCore/Reporting/ReadModels/AttendanceReportReadModel.php create mode 100644 app/Domain/SchoolCore/Reporting/ReadModels/CommunicationReportReadModel.php create mode 100644 app/Domain/SchoolCore/Reporting/ReadModels/FinanceReportReadModel.php create mode 100644 app/Domain/SchoolCore/Reporting/ReadModels/StudentReportReadModel.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Attendance/AttendanceSummaryReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Attendance/StaffAttendanceSummaryReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Attendance/StudentAttendanceHistoryReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/BaseReportDefinition.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Communication/MessageDeliveryReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Communication/RecipientEngagementReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Finance/InvoiceSummaryReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Finance/OutstandingBalanceReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Finance/PaymentSummaryReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Students/GuardianContactReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Students/StudentEnrollmentReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Reports/Students/StudentRosterReport.php create mode 100644 app/Domain/SchoolCore/Reporting/Repositories/ReportReadRepository.php create mode 100644 app/Domain/SchoolCore/Reporting/Repositories/ReportSnapshotRepository.php create mode 100644 app/Domain/SchoolCore/Reporting/Repositories/ScheduledReportRepository.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/DefaultReportAuthorizationPolicy.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportAuditLogger.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportCacheService.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportExportService.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportRegistry.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportRunner.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportService.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ReportSnapshotService.php create mode 100644 app/Domain/SchoolCore/Reporting/Services/ScheduledReportService.php create mode 100644 app/Domain/SchoolCore/Reporting/Support/ReportColumnFormatter.php create mode 100644 app/Domain/SchoolCore/Reporting/Support/ReportContext.php create mode 100644 app/Domain/SchoolCore/Reporting/Support/ReportExportFilenameBuilder.php create mode 100644 app/Domain/SchoolCore/Reporting/Support/ReportFilterValidator.php create mode 100644 app/Domain/SchoolCore/Reporting/Support/ReportQueryBuilder.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/AssignmentServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/EnrollmentServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/GuardianServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/HouseholdServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/PromotionPolicyContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/PromotionServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/StudentIdentifierGeneratorContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/StudentLifecycleAuditLoggerContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/StudentProfileServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/StudentReadServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/Contracts/StudentServiceContract.php create mode 100644 app/Domain/SchoolCore/Students/DTO/AssignmentData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/AssignmentResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/BulkPromotionData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/BulkPromotionResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/CreateGuardianData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/CreateStudentData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/EnrollmentData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/EnrollmentResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/EnrollmentView.php create mode 100644 app/Domain/SchoolCore/Students/DTO/GuardianLinkResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/GuardianResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/HouseholdData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/HouseholdResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/LinkGuardianData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/PromotionData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/PromotionEvaluationResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/PromotionResultData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/StudentIdentifier.php create mode 100644 app/Domain/SchoolCore/Students/DTO/StudentProfileData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/StudentResult.php create mode 100644 app/Domain/SchoolCore/Students/DTO/StudentSearchQuery.php create mode 100644 app/Domain/SchoolCore/Students/DTO/StudentStatusTransitionData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/StudentView.php create mode 100644 app/Domain/SchoolCore/Students/DTO/UpdateGuardianData.php create mode 100644 app/Domain/SchoolCore/Students/DTO/UpdateStudentData.php create mode 100644 app/Domain/SchoolCore/Students/Data/CreateStudentData.php create mode 100644 app/Domain/SchoolCore/Students/Data/StudentReadModel.php create mode 100644 app/Domain/SchoolCore/Students/Data/UpdateStudentData.php create mode 100644 app/Domain/SchoolCore/Students/Enums/AssignmentStatus.php create mode 100644 app/Domain/SchoolCore/Students/Enums/EnrollmentStatus.php create mode 100644 app/Domain/SchoolCore/Students/Enums/GuardianRelationshipType.php create mode 100644 app/Domain/SchoolCore/Students/Enums/PromotionResult.php create mode 100644 app/Domain/SchoolCore/Students/Enums/StudentAuditAction.php create mode 100644 app/Domain/SchoolCore/Students/Enums/StudentStatus.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/EnrollmentException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/GuardianException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/InvalidStudentStatusTransitionException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/PromotionException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/StudentException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/StudentIdentifierCollisionException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/StudentNotFoundException.php create mode 100644 app/Domain/SchoolCore/Students/Exceptions/UnauthorizedStudentAccessException.php create mode 100644 app/Domain/SchoolCore/Students/Policies/DefaultEnrollmentPolicy.php create mode 100644 app/Domain/SchoolCore/Students/Policies/DefaultPromotionPolicy.php create mode 100644 app/Domain/SchoolCore/Students/Policies/DefaultStudentIdentifierGenerator.php create mode 100644 app/Domain/SchoolCore/Students/README.md create mode 100644 app/Domain/SchoolCore/Students/Repositories/AssignmentRepository.php create mode 100644 app/Domain/SchoolCore/Students/Repositories/EnrollmentRepository.php create mode 100644 app/Domain/SchoolCore/Students/Repositories/GuardianRepository.php create mode 100644 app/Domain/SchoolCore/Students/Repositories/HouseholdRepository.php create mode 100644 app/Domain/SchoolCore/Students/Repositories/StudentReadRepository.php create mode 100644 app/Domain/SchoolCore/Students/Repositories/StudentRepository.php create mode 100644 app/Domain/SchoolCore/Students/Services/AssignmentService.php create mode 100644 app/Domain/SchoolCore/Students/Services/EnrollmentService.php create mode 100644 app/Domain/SchoolCore/Students/Services/GuardianService.php create mode 100644 app/Domain/SchoolCore/Students/Services/HouseholdService.php create mode 100644 app/Domain/SchoolCore/Students/Services/PromotionService.php create mode 100644 app/Domain/SchoolCore/Students/Services/SequentialStudentIdentifierGenerator.php create mode 100644 app/Domain/SchoolCore/Students/Services/StudentIdentifierService.php create mode 100644 app/Domain/SchoolCore/Students/Services/StudentLifecycleAuditLogger.php create mode 100644 app/Domain/SchoolCore/Students/Services/StudentProfileService.php create mode 100644 app/Domain/SchoolCore/Students/Services/StudentReadService.php create mode 100644 app/Domain/SchoolCore/Students/Services/StudentService.php create mode 100644 app/Domain/SchoolCore/Students/StudentListQueryService.php create mode 100644 app/Domain/SchoolCore/Students/StudentReadController.php create mode 100644 app/Domain/SchoolCore/Students/Support/StudentLifecycleStateMachine.php create mode 100644 app/Domain/SchoolCore/Students/Support/StudentSearchNormalizer.php create mode 100644 app/Domain/SchoolCore/Students/Support/StudentTransactionRunner.php create mode 100644 app/Domain/SchoolCore/Support/AppliesSchoolContext.php create mode 100644 app/Domain/SchoolCore/Support/README.md create mode 100644 app/Domain/SchoolCore/Support/SchoolContextAware.php create mode 100644 app/Domain/Shared/Contracts/README.md create mode 100644 app/Domain/Shared/Exceptions/README.md create mode 100644 app/Domain/Shared/Support/README.md create mode 100644 app/Domain/Shared/ValueObjects/README.md create mode 100644 app/Http/Controllers/Api/V2/Attendance/ScanController.php create mode 100644 app/Http/Controllers/Api/V2/BaseApiController.php create mode 100644 app/Http/Controllers/Api/V2/Communication/BulkSendController.php create mode 100644 app/Http/Controllers/Api/V2/Communication/RecipientPreviewController.php create mode 100644 app/Http/Controllers/Api/V2/Finance/PaymentController.php create mode 100644 app/Http/Controllers/Api/V2/Finance/PaymentFileController.php create mode 100644 app/Http/Controllers/Api/V2/IslamicSundaySchool/QuranProgressController.php create mode 100644 app/Http/Controllers/Api/V2/Reporting/ReportController.php create mode 100644 app/Http/Controllers/Api/V2/Reporting/ReportExportController.php create mode 100644 app/Http/Controllers/Api/V2/Students/StudentController.php create mode 100644 app/Http/Controllers/Attendance/ScannerController.php create mode 100644 app/Http/Controllers/Attendance/StaffAttendanceController.php create mode 100644 app/Http/Controllers/Attendance/StudentAttendanceController.php create mode 100644 app/Http/Controllers/Communication/BulkMessageController.php create mode 100644 app/Http/Controllers/Communication/MessageTemplateController.php create mode 100644 app/Http/Controllers/Communication/RecipientPreviewController.php create mode 100644 app/Http/Controllers/Finance/PaymentFileController.php create mode 100644 app/Http/Controllers/Reporting/ReportController.php create mode 100644 app/Http/Controllers/Reporting/ReportExportController.php create mode 100644 app/Http/Controllers/Reporting/ScheduledReportController.php create mode 100644 app/Http/Controllers/StudentCreateController.php create mode 100644 app/Http/Controllers/Students/AssignmentController.php create mode 100644 app/Http/Controllers/Students/EnrollmentController.php create mode 100644 app/Http/Controllers/Students/GuardianController.php create mode 100644 app/Http/Controllers/Students/IslamicSundaySchoolStudentProfileController.php create mode 100644 app/Http/Controllers/Students/PromotionController.php create mode 100644 app/Http/Controllers/Students/StudentLifecycleController.php create mode 100644 app/Http/Middleware/DeprecationHeaders.php create mode 100644 app/Http/Middleware/EnsureDomainProfile.php create mode 100644 app/Http/Middleware/ResolveSchoolContext.php create mode 100644 app/Http/Requests/Attendance/AttendanceSessionRequest.php create mode 100644 app/Http/Requests/Attendance/AttendanceSummaryRequest.php create mode 100644 app/Http/Requests/Attendance/ManualStaffAttendanceOverrideRequest.php create mode 100644 app/Http/Requests/Attendance/ManualStudentAttendanceOverrideRequest.php create mode 100644 app/Http/Requests/Attendance/ProcessScanRequest.php create mode 100644 app/Http/Requests/Attendance/RecordStaffAttendanceRequest.php create mode 100644 app/Http/Requests/Attendance/RecordStudentAttendanceRequest.php create mode 100644 app/Http/Requests/Communication/BulkSendMessageRequest.php create mode 100644 app/Http/Requests/Communication/CancelScheduledMessageRequest.php create mode 100644 app/Http/Requests/Communication/CreateMessageTemplateRequest.php create mode 100644 app/Http/Requests/Communication/DeliveryReceiptRequest.php create mode 100644 app/Http/Requests/Communication/RecipientPreviewRequest.php create mode 100644 app/Http/Requests/Communication/ScheduleMessageRequest.php create mode 100644 app/Http/Requests/Communication/SendMessageRequest.php create mode 100644 app/Http/Requests/Communication/UpdateCommunicationPreferenceRequest.php create mode 100644 app/Http/Requests/Communication/UpdateMessageTemplateRequest.php create mode 100644 app/Http/Requests/Finance/DownloadPaymentFileRequest.php create mode 100644 app/Http/Requests/Finance/EditPaymentRequest.php create mode 100644 app/Http/Requests/Finance/RecordPaymentRequest.php create mode 100644 app/Http/Requests/Finance/RefundPaymentRequest.php create mode 100644 app/Http/Requests/Reporting/CancelScheduledReportRequest.php create mode 100644 app/Http/Requests/Reporting/ExportReportRequest.php create mode 100644 app/Http/Requests/Reporting/ListReportsRequest.php create mode 100644 app/Http/Requests/Reporting/RunReportRequest.php create mode 100644 app/Http/Requests/Reporting/ScheduleReportRequest.php create mode 100644 app/Http/Requests/Reporting/ViewReportSnapshotRequest.php create mode 100644 app/Http/Requests/Students/AssignmentRequest.php create mode 100644 app/Http/Requests/Students/BulkPromotionRequest.php create mode 100644 app/Http/Requests/Students/CreateGuardianRequest.php create mode 100644 app/Http/Requests/Students/CreateHouseholdRequest.php create mode 100644 app/Http/Requests/Students/CreateStudentRequest.php create mode 100644 app/Http/Requests/Students/EnrollmentRequest.php create mode 100644 app/Http/Requests/Students/IslamicSundaySchoolStudentProfileRequest.php create mode 100644 app/Http/Requests/Students/LinkGuardianRequest.php create mode 100644 app/Http/Requests/Students/PromotionRequest.php create mode 100644 app/Http/Requests/Students/StudentSearchRequest.php create mode 100644 app/Http/Requests/Students/StudentStatusTransitionRequest.php create mode 100644 app/Http/Requests/Students/UpdateStudentRequest.php create mode 100644 app/Http/Resources/V2/Attendance/AttendanceRecordResource.php create mode 100644 app/Http/Resources/V2/Attendance/ScanResultResource.php create mode 100644 app/Http/Resources/V2/Communication/MessageResource.php create mode 100644 app/Http/Resources/V2/Communication/RecipientPreviewResource.php create mode 100644 app/Http/Resources/V2/Finance/InvoiceResource.php create mode 100644 app/Http/Resources/V2/Finance/PaymentResource.php create mode 100644 app/Http/Resources/V2/IslamicSundaySchool/IslamicSundaySchoolStudentProfileResource.php create mode 100644 app/Http/Resources/V2/IslamicSundaySchool/QuranProgressReportResource.php create mode 100644 app/Http/Resources/V2/Reporting/ReportExportResource.php create mode 100644 app/Http/Resources/V2/Reporting/ReportResultResource.php create mode 100644 app/Http/Resources/V2/Students/EnrollmentResource.php create mode 100644 app/Http/Resources/V2/Students/GuardianResource.php create mode 100644 app/Http/Resources/V2/Students/StudentResource.php create mode 100644 app/Jobs/Release/RunMigrationValidationJob.php create mode 100644 app/Jobs/Release/RunShadowComparisonJob.php create mode 100644 app/Providers/IslamicSundaySchoolServiceProvider.php create mode 100644 app/Providers/SchoolContextServiceProvider.php create mode 100644 app/Providers/SchoolCoreAttendanceServiceProvider.php create mode 100644 app/Providers/SchoolCoreCommunicationServiceProvider.php create mode 100644 app/Providers/SchoolCoreFinanceServiceProvider.php create mode 100644 app/Providers/SchoolCoreReportingServiceProvider.php create mode 100644 app/Providers/SchoolCoreServiceProvider.php create mode 100644 app/Providers/SchoolCoreStudentServiceProvider.php create mode 100644 app/Support/Api/ApiExceptionRenderer.php create mode 100644 app/Support/Api/ApiResponse.php create mode 100644 app/Support/Api/ControllerClassification.php create mode 100644 app/Support/Api/RouteRisk.php create mode 100644 app/Support/Architecture/ArchitectureFileFinder.php create mode 100644 app/Support/Architecture/ArchitectureRule.php create mode 100644 app/Support/Architecture/ArchitectureRuleRegistry.php create mode 100644 app/Support/Architecture/ArchitectureScanner.php create mode 100644 app/Support/Architecture/ArchitectureViolation.php create mode 100644 app/Support/Architecture/DependencyMapGenerator.php create mode 100644 app/Support/Release/FeatureFlagDefinition.php create mode 100644 app/Support/Release/FeatureFlagRegistry.php create mode 100644 app/Support/Release/MigrationValidationRunner.php create mode 100644 app/Support/Release/ReleaseReadinessGate.php create mode 100644 app/Support/Release/ShadowComparisonResult.php create mode 100644 app/Support/Release/ShadowComparisonRunner.php create mode 100644 app/Support/Release/ValidationResult.php create mode 100644 app/helpers.php create mode 100755 artisan create mode 100644 bootstrap/app.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 bootstrap/providers.php create mode 100644 composer.autoload.snippet.json create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 composer.phase10.scripts.snippet.json create mode 100644 composer.phase9.scripts.snippet.json create mode 100644 config/api_controller_classification.php create mode 100644 config/architecture_governance.php create mode 100644 config/modular_release.php create mode 100644 config/module_ownership.php create mode 100644 config/release_readiness.php create mode 100644 config/school_context.php create mode 100644 database/migrations/2026_05_29_000002_add_unique_student_identifier_per_school.php create mode 100644 database/migrations/2026_05_29_000003_create_finance_audit_and_payment_file_tables.php create mode 100644 database/migrations/2026_05_29_000004_add_modular_finance_indexes.php create mode 100644 database/migrations/2026_05_29_000005_create_attendance_core_tables.php create mode 100644 database/migrations/2026_05_29_000006_create_attendance_scan_audit_idempotency_tables.php create mode 100644 database/migrations/2026_05_29_000007_create_student_lifecycle_core_tables.php create mode 100644 database/migrations/2026_05_29_000008_create_student_enrollment_assignment_audit_tables.php create mode 100644 database/migrations/2026_05_29_000009_create_islamic_sunday_school_student_profiles.php create mode 100644 database/migrations/2026_05_29_000010_create_communication_core_tables.php create mode 100644 database/migrations/2026_05_29_000011_create_communication_delivery_template_preference_tables.php create mode 100644 database/migrations/2026_05_29_000012_create_reporting_core_tables.php create mode 100644 database/migrations/2026_05_29_000013_create_scheduled_reports_table.php create mode 100644 database/migrations/2026_05_29_000014_create_api_route_inventory_snapshots_table.php create mode 100644 docs/api-deprecations.md create mode 100644 docs/api-route-inventory.md create mode 100644 docs/architecture/dependency-map.md create mode 100644 docs/architecture/module-boundaries.md create mode 100644 docs/architecture/module-ownership.md create mode 100644 docs/controller-classification.md create mode 100644 docs/deprecations.md create mode 100644 docs/modules/attendance.md create mode 100644 docs/modules/communication.md create mode 100644 docs/modules/finance.md create mode 100644 docs/modules/islamic-sunday-school.md create mode 100644 docs/modules/reporting.md create mode 100644 docs/modules/students.md create mode 100644 docs/openapi.phase8.yaml create mode 100644 docs/release-checklist.md create mode 100644 docs/release/api-client-migration.md create mode 100644 docs/release/feature-flag-rollout-map.md create mode 100644 docs/release/incident-response.md create mode 100644 docs/release/islamic-sunday-school-production-rollout.md create mode 100644 docs/release/legacy-removal-plan.md create mode 100644 docs/release/migration-validation.md create mode 100644 docs/release/monitoring-dashboards.md create mode 100644 docs/release/performance-readiness.md create mode 100644 docs/release/pilot-rollout-plan.md create mode 100644 docs/release/post-release-review-template.md create mode 100644 docs/release/production-readiness-review.md create mode 100644 docs/release/rollback-playbooks.md create mode 100644 docs/release/security-readiness.md create mode 100644 docs/release/shadow-comparison-plan.md create mode 100644 docs/release/support-playbooks.md create mode 100644 docs/release/training-guides.md create mode 100644 docs/release/uat-scenarios.md create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 public/.htaccess create mode 100644 public/favicon.ico create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 resources/css/app.css create mode 100644 resources/js/app.js create mode 100644 resources/views/welcome.blade.php create mode 100644 routes/api_phase8.php create mode 100644 routes/attendance_phase4.php create mode 100644 routes/communication_phase6.php create mode 100644 routes/finance_phase3.php create mode 100644 routes/reporting_phase7.php create mode 100644 routes/school_context_examples.php create mode 100644 routes/student_lifecycle_phase5.php create mode 100644 storage/app/generated/api-route-inventory.json create mode 100644 storage/app/generated/architecture-scan-baseline.json create mode 100644 storage/app/generated/dependency-map.json create mode 100644 storage/app/generated/release-feature-flags.json create mode 100644 storage/app/generated/release-validation/validation-summary.json create mode 100644 storage/app/generated/shadow-comparisons/shadow-summary.json create mode 100644 tests/Architecture/AttendanceBoundaryTest.php create mode 100644 tests/Architecture/CommunicationBoundaryTest.php create mode 100644 tests/Architecture/Concerns/ScansArchitecture.php create mode 100644 tests/Architecture/ControllerBoundaryTest.php create mode 100644 tests/Architecture/DeprecationPolicyTest.php create mode 100644 tests/Architecture/FinanceBoundaryTest.php create mode 100644 tests/Architecture/IslamicSundaySchoolBoundaryTest.php create mode 100644 tests/Architecture/ModuleBoundaryTest.php create mode 100644 tests/Architecture/Phase10ReleaseReadinessArchitectureTest.php create mode 100644 tests/Architecture/Phase2BoundaryArchitectureTest.php create mode 100644 tests/Architecture/Phase3FinanceArchitectureTest.php create mode 100644 tests/Architecture/Phase4AttendanceArchitectureTest.php create mode 100644 tests/Architecture/Phase5StudentLifecycleArchitectureTest.php create mode 100644 tests/Architecture/Phase6CommunicationArchitectureTest.php create mode 100644 tests/Architecture/Phase7ReportingArchitectureTest.php create mode 100644 tests/Architecture/Phase8ControllerApiArchitectureTest.php create mode 100644 tests/Architecture/ReportingBoundaryTest.php create mode 100644 tests/Architecture/RouteInventoryTest.php create mode 100644 tests/Architecture/SchoolCoreArchitectureTest.php create mode 100644 tests/Architecture/ServiceBoundaryTest.php create mode 100644 tests/Architecture/StudentBoundaryTest.php create mode 100644 tests/Feature/Api/Phase8ApiEnvelopeTest.php create mode 100644 tests/Feature/Release/FeatureFlagRolloutMapTest.php create mode 100644 tests/Feature/Release/MigrationValidationScaffoldTest.php create mode 100644 tests/Feature/SchoolCore/Attendance/ScannerDelegationTest.php create mode 100644 tests/Feature/SchoolCore/Communication/BulkSendRequiresPreviewTest.php create mode 100644 tests/Feature/SchoolCore/Finance/PaymentFileRouteTest.php create mode 100644 tests/Feature/SchoolCore/PaymentFilePolicyTest.php create mode 100644 tests/Feature/SchoolCore/Reporting/ReportingPhase7ScaffoldTest.php create mode 100644 tests/Feature/SchoolCore/ResolveSchoolContextMiddlewareTest.php create mode 100644 tests/Feature/SchoolCore/StudentIdentifierConcurrencyTest.php create mode 100644 tests/Feature/SchoolCore/Students/StudentLifecycleDelegationTest.php create mode 100644 tests/Unit/IslamicSundaySchool/Attendance/IslamicSundaySchoolAttendanceStatusPolicyTest.php create mode 100644 tests/Unit/IslamicSundaySchool/AttendancePolicyTest.php create mode 100644 tests/Unit/IslamicSundaySchool/Communication/IslamicSundaySchoolCommunicationExtensionTest.php create mode 100644 tests/Unit/IslamicSundaySchool/Reporting/IslamicSundaySchoolReportAvailabilityTest.php create mode 100644 tests/Unit/IslamicSundaySchool/Students/IslamicSundaySchoolStudentIdentifierGeneratorTest.php create mode 100644 tests/Unit/SchoolCore/Attendance/DefaultAttendanceStatusPolicyTest.php create mode 100644 tests/Unit/SchoolCore/Attendance/ScanFingerprintTest.php create mode 100644 tests/Unit/SchoolCore/Communication/RecipientDedupeServiceTest.php create mode 100644 tests/Unit/SchoolCore/Context/SchoolContextTest.php create mode 100644 tests/Unit/SchoolCore/Context/SchoolContextValidatorTest.php create mode 100644 tests/Unit/SchoolCore/Finance/BalanceCalculatorTest.php create mode 100644 tests/Unit/SchoolCore/Finance/MoneyTest.php create mode 100644 tests/Unit/SchoolCore/Reporting/ReportRegistryTest.php create mode 100644 tests/Unit/SchoolCore/Students/DefaultStudentIdentifierGeneratorTest.php create mode 100644 tests/Unit/SchoolCore/Students/SequentialStudentIdentifierGeneratorTest.php create mode 100644 tests/Unit/SchoolCore/Students/StudentLifecycleStateMachineTest.php create mode 100644 vite.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6df8428 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[{compose,docker-compose}.{yml,yaml}] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7be55e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.codex +/.cursor/ +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..495a6af --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +audit=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b6b7d9 --- /dev/null +++ b/README.md @@ -0,0 +1,289 @@ +# Laravel SchoolContext Phase 1 Implementation + +This package-style scaffold implements Phase 1 of the SchoolContext foundation for a Laravel app. + +It includes: + +- immutable `SchoolContext` value object; +- context factory, resolver, validator, store, and exception; +- `ResolveSchoolContext` middleware; +- service provider bindings; +- helper for legacy adapters only; +- query scoping trait; +- first student read/list slice; +- payment-file access slice that resolves files through payment context, not arbitrary filenames; +- context payload helper for jobs/events; +- unit, feature, and architecture tests. + +## Install into an existing Laravel app + +Copy the `app`, `config`, `routes`, and `tests` folders into your Laravel project. + +Register the provider in `config/app.php` if your Laravel version does not auto-discover app providers: + +```php +App\Providers\SchoolContextServiceProvider::class, +``` + +For Laravel 10 and older, you may also register the middleware alias in `app/Http/Kernel.php`: + +```php +protected $middlewareAliases = [ + 'school.context' => \App\Http\Middleware\ResolveSchoolContext::class, +]; +``` + +For Laravel 11+, register it in `bootstrap/app.php`: + +```php +->withMiddleware(function (Middleware $middleware) { + $middleware->alias([ + 'school.context' => \App\Http\Middleware\ResolveSchoolContext::class, + ]); +}) +``` + +Load the example routes only after adapting controllers/model names to your app: + +```php +require base_path('routes/school_context_examples.php'); +``` + +## Required model assumptions + +This scaffold expects these model concepts to exist or be adapted: + +- `App\Models\School` with optional `timezone`, `locale`, `currency`, `domain_profile`, `active` fields. +- `App\Models\Configuration::getConfig($key)` during migration only. +- `App\Models\Student` with `school_id`. +- `App\Models\Payment` with `school_id` and a `files()` relationship. +- user model with `school_id`, optional `role_id`, optional `roles()`, optional `schools()`, optional `is_platform_admin`. + +## Migration policy + +New SchoolCore services should accept `SchoolContext` explicitly. Do not call `auth()`, `request()`, or `Configuration::getConfig()` inside SchoolCore services. The architecture test exists to catch that predictable human shortcut. + +## Route middleware order + +Use this order: + +```txt +auth:api +school.context +permission/policy/controller +``` + +Do not run `school.context` before authentication for protected routes. + +## Important note + +`SchoolContext::isIslamicSundaySchool()` exists as a convenience check, but SchoolCore should not branch into Islamic-specific concepts. Domain-specific behavior belongs in policies, bindings, labels, calendars, and extension providers. + + +# Phase 2 Add-on: Core Contracts and Module Boundaries + +This scaffold now includes the Phase 2 contract layer: + +- neutral `SchoolCore` contracts for students, guardians, enrollment, attendance, academics, finance, files, communication, and reporting; +- immutable DTO/read-model skeletons for cross-module payloads; +- `SchoolCoreServiceProvider` with default core bindings; +- `IslamicSundaySchoolServiceProvider` with extension policy/provider overrides; +- race-safe student identifier generation through `StudentIdentifierGeneratorContract`; +- payment file access through `FileAccessPolicyContract` and `FileStorageServiceContract`; +- attendance policy and academic calendar provider stubs; +- architecture tests for forbidden dependency direction, raw request leakage, `auth()` leakage, and non-neutral contract vocabulary; +- a migration stub for `unique(school_id, school_id_number)`. + +## Register Phase 2 providers + +Add these after `SchoolContextServiceProvider`: + +```php +App\Providers\SchoolCoreServiceProvider::class, +App\Providers\IslamicSundaySchoolServiceProvider::class, +``` + +Enable the extension policy overrides only when the school profile requires them: + +```env +ISLAMIC_SUNDAY_SCHOOL_EXTENSION_ENABLED=true +``` + +## Student identifier slice + +`StudentService` creates the student row first, then calls `StudentIdentifierGeneratorContract` with the persisted ID. This avoids the classic `max(id) + 1` race. The migration stub adds a database unique constraint, because relying on application code alone for uniqueness is how duplicates sneak in wearing a fake mustache. + +## Payment file access slice + +Use payment IDs and stored file references. Do not serve files by guessed filename. The policy checks school context first, then role/relationship metadata. Adapt relationship metadata to your actual finance schema. + +## Attendance policy stub + +Core scanner logic should call `AttendancePolicyContract` and `AcademicCalendarProviderContract`. The extension provider supplies domain-specific attendance behavior without making `SchoolCore` speak extension vocabulary. + + +## Phase 3: Modular Finance Extraction + +Phase 3 adds a reusable `SchoolCore\Finance` module. The core owns financial correctness: invoices, payments, refunds, payment files, audit events, balance calculation, idempotency, and transaction boundaries. Islamic Sunday School behavior is extension-owned through finance policies and reports. + +Register these providers after the Phase 1/2 providers: + +```php +App\Providers\SchoolCoreFinanceServiceProvider::class, +App\Domain\IslamicSundaySchool\Finance\Providers\IslamicSundaySchoolFinanceServiceProvider::class, +``` + +Add `routes/finance_phase3.php` to your route loader, then migrate one vertical slice at a time. Start with payment-file access, then payment recording, payment editing, invoice recalculation, refunds/reversals, and only then reports. Reports consume finance truth. They do not invent it, despite humanity's long-running spreadsheet addiction. + +Important integration notes: + +- Finance services require `SchoolContext`. +- Finance mutation services use `FinanceTransactionRunner`. +- Payment files are downloaded by payment ID, never by arbitrary filename. +- `Money` stores integer minor units and rejects float math. +- `finance_audit_logs` are append-only. +- Existing legacy finance methods should be labeled `canonical`, `adapter`, `deprecated`, or `remove` before migration continues. + + +## Phase 4: Modular Attendance and Scanner Extraction + +Phase 4 adds `SchoolCore\\Attendance` as the neutral attendance/scanner module. It includes scanner delegation, school-scoped badge lookup, scan idempotency, student/staff attendance services, session resolution, status policies, audit logging, migrations, HTTP requests/controllers, route examples, provider bindings, and Islamic Sunday School extension overrides. + +Register these providers after the base SchoolCore providers: + +```php +App\\Providers\\SchoolCoreAttendanceServiceProvider::class, +App\\Domain\\IslamicSundaySchool\\Attendance\\Providers\\IslamicSundaySchoolAttendanceServiceProvider::class, // only for the Islamic Sunday School profile/app +``` + +Load the example routes from `routes/attendance_phase4.php` or merge them into your API route file. Keep legacy scanner endpoints as adapters only until compatibility tests pass. Controllers should delegate to `ScannerServiceContract`, not recreate attendance logic in private methods. + + +## Phase 5: Modular Student Lifecycle Extraction + +Phase 5 adds a neutral `SchoolCore\Students` lifecycle module for race-safe identifiers, student creation/update/status transitions, guardians, households, enrollment, assignments, promotion, read models, audit logging, and Islamic Sunday School profile extension points. + +Core student lifecycle stays reusable: no Qur'an, Arabic, Islamic studies, halaqa, masjid/community, or sensitive religious/community note fields belong in `SchoolCore\Students`. Those live in `App\Domain\IslamicSundaySchool\Students`. The point is not aesthetic purity; it is avoiding a global student model that secretly belongs to one domain and then acts innocent. + +Register: + +```php +App\Providers\SchoolCoreStudentServiceProvider::class, +App\Domain\IslamicSundaySchool\Students\Providers\IslamicSundaySchoolStudentServiceProvider::class, +``` + +Example routes: `routes/student_lifecycle_phase5.php`. + + +## Phase 6: Modular Communication Extraction + +Phase 6 adds `SchoolCore\Communication` as the neutral communication engine. Bulk communication now uses a preview-first path: resolve recipients, dedupe, apply preferences, store a snapshot, confirm with a token, then send through channel contracts. This prevents accidental mass sends, duplicate deliveries, and cross-school recipient leakage. + +Register: + +```php +App\Providers\SchoolCoreCommunicationServiceProvider::class, +App\Domain\IslamicSundaySchool\Communication\Providers\IslamicSundaySchoolCommunicationServiceProvider::class, +``` + +Routes are in `routes/communication_phase6.php`. Include them only after authentication and `ResolveSchoolContext` are active. + +Key rules: bulk sends require `preview_id` and `confirmation_token`; recipient queries live in resolvers, not controllers; email/SMS/WhatsApp/notification delivery lives behind channel contracts; Islamic Sunday School recipient behavior is extension-owned. + + +## Phase 7: Modular Reporting Extraction + +Phase 7 adds `SchoolCore\Reporting` as the reporting boundary. Reports are read-only, require `SchoolContext`, and must consume read models instead of inventing finance, attendance, communication, or student lifecycle truth with ad hoc queries. + +Added provider: + +```php +App\Providers\SchoolCoreReportingServiceProvider::class, +App\Domain\IslamicSundaySchool\Reporting\Providers\IslamicSundaySchoolReportingServiceProvider::class, +``` + +Added route file: + +```php +require base_path('routes/reporting_phase7.php'); +``` + +The core reporting registry supports tagged report definitions using `schoolcore.reports`. Islamic Sunday School reports register through the extension provider and are only available when the context domain profile is `islamic_sunday_school`. + +Phase 7 deliberately treats exports as controlled access, not a casual CSV vending machine. Export paths go through `ReportExportServiceContract`, audit hooks exist, snapshots are school-scoped, and architecture tests prevent `SchoolCore\Reporting` from importing extension classes. + + +## Phase 8: Controller and API Cleanup + +Phase 8 adds the HTTP cleanup layer: + +- v2 route file: `routes/api_phase8.php` +- standardized API response envelope: `App\Support\Api\ApiResponse` +- centralized exception rendering helper: `App\Support\Api\ApiExceptionRenderer` +- domain-profile middleware: `EnsureDomainProfile` +- deprecation header middleware: `DeprecationHeaders` +- route inventory generator: `php artisan api:route-inventory --markdown` +- controller classification map: `config/api_controller_classification.php` +- OpenAPI baseline: `docs/openapi.phase8.yaml` +- route inventory docs: `docs/api-route-inventory.md` +- v2 canonical controllers for finance, attendance, students, communication, reporting, and Islamic Sunday School extension routes +- architecture tests blocking raw DB/provider/controller business logic patterns + +Phase 8 does not erase legacy routes. It creates canonical v2 delivery adapters and gives old routes a sane path to become adapters or deprecated endpoints. Because breaking clients for “architecture” is still breaking clients, just with nicer folder names. + + +## Phase 9: Boundary Enforcement and Modular Governance + +Phase 9 adds enforceable modular governance on top of the Phase 1-8 scaffold. + +Installed governance pieces: + +- `app:architecture-scan` artisan command for forbidden imports, HTTP leakage, controller/provider calls, finance float usage, unsafe file routes, extension vocabulary, and critical modular rules. +- `app:dependency-map` command for dependency map artifacts. +- `app:route-inventory-check` command for route inventory governance. +- `app:docs-coverage` command for route/OpenAPI/deprecation coverage checks. +- `tests/Architecture/*BoundaryTest.php` suites for module, controller, service, finance, attendance, student, communication, reporting, Islamic Sunday School, route inventory, and deprecation rules. +- `.github/CODEOWNERS`, PR checklist, architecture workflow, and release gate workflow. +- `docs/architecture/*`, `docs/modules/*`, `docs/deprecations.md`, and `docs/release-checklist.md`. + +Recommended rollout: + +```bash +php artisan app:architecture-scan --fail-on=none +php artisan test tests/Architecture +php artisan api:route-inventory --markdown +php artisan app:route-inventory-check +php artisan app:dependency-map --markdown +php artisan app:docs-coverage +``` + +Start in warning mode for existing legacy code. Critical new violations should fail immediately. Letting legacy code break every build on day one is not governance; it is a morale experiment with predictable results. + + +## Phase 10: Migration, Release, and Production Readiness + +Phase 10 adds production rollout governance for the modular platform: + +- feature flag rollout map +- migration validation commands +- shadow comparison scaffolding +- release readiness gate +- rollback playbooks +- monitoring and alerting docs +- security/performance readiness checklists +- UAT scenarios +- support playbooks +- pilot and Islamic Sunday School production rollout plans +- legacy unsafe route audit + +Useful commands: + +```bash +php artisan app:release-feature-flags +php artisan app:release-migration-validate +php artisan app:release-shadow-compare +php artisan app:legacy-unsafe-route-audit +php artisan app:release-readiness-gate --warning-mode +``` + +Phase 10 is not a magic permission slip to ship everything. It is the checklist that stops the team from discovering rollback strategy during an incident, which is traditionally when everyone becomes philosophical and useless. diff --git a/app/Console/Commands/ArchitectureScanCommand.php b/app/Console/Commands/ArchitectureScanCommand.php new file mode 100644 index 0000000..b633f7e --- /dev/null +++ b/app/Console/Commands/ArchitectureScanCommand.php @@ -0,0 +1,51 @@ +scan(); + + if ($this->option('format') === 'json') { + $this->line(json_encode(array_map(fn ($v) => $v->toArray(), $violations), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } else { + if ($violations === []) { + $this->info('No architecture violations found. Suspiciously civilized.'); + } + foreach ($violations as $violation) { + $this->line(sprintf( + '[%s] %s %s:%d %s', + strtoupper($violation->severity), + $violation->ruleId, + $violation->file, + $violation->line, + $violation->message + )); + if ($violation->suggestedFix) { + $this->line(' Fix: '.$violation->suggestedFix); + } + } + } + + $failOn = (string) $this->option('fail-on'); + if ($failOn === 'none') { + return self::SUCCESS; + } + + foreach ($violations as $violation) { + if ($violation->severity === 'error' || ($failOn === 'warning' && $violation->severity === 'warning')) { + return self::FAILURE; + } + } + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/DependencyMapCommand.php b/app/Console/Commands/DependencyMapCommand.php new file mode 100644 index 0000000..0f0e543 --- /dev/null +++ b/app/Console/Commands/DependencyMapCommand.php @@ -0,0 +1,36 @@ +generate(); + Storage::disk('local')->put('generated/dependency-map.json', json_encode($map, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + if ($this->option('markdown')) { + $lines = ['# Dependency Map', '', 'Generated from PHP imports. Treat this as a smoke alarm, not a philosophical essay.', '']; + foreach ($map['files'] as $file => $imports) { + $lines[] = '## `'.$file.'`'; + $lines[] = ''; + foreach ($imports as $import) { + $lines[] = '- `'.$import.'`'; + } + $lines[] = ''; + } + @mkdir(base_path('docs/architecture'), 0777, true); + file_put_contents(base_path('docs/architecture/dependency-map.md'), implode(PHP_EOL, $lines).PHP_EOL); + } + + $this->info('Dependency map generated. Now the imports have fewer places to hide.'); + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/DocumentationCoverageCommand.php b/app/Console/Commands/DocumentationCoverageCommand.php new file mode 100644 index 0000000..d46d24a --- /dev/null +++ b/app/Console/Commands/DocumentationCoverageCommand.php @@ -0,0 +1,45 @@ +option('inventory')); + $openApiPath = base_path((string) $this->option('openapi')); + $deprecationsPath = base_path('docs/deprecations.md'); + + if (! is_file($inventoryPath) || ! is_file($openApiPath)) { + $this->error('Inventory or OpenAPI file is missing. Documentation cannot be trusted by vibes alone.'); + return self::FAILURE; + } + + $rows = json_decode(file_get_contents($inventoryPath) ?: '[]', true) ?: []; + $openApi = file_get_contents($openApiPath) ?: ''; + $deprecations = is_file($deprecationsPath) ? (file_get_contents($deprecationsPath) ?: '') : ''; + $failures = []; + + foreach ($rows as $row) { + $uri = '/'.ltrim((string) ($row['uri'] ?? ''), '/'); + $status = (string) ($row['canonical_status'] ?? 'unknown'); + if ($status === 'canonical' && ! str_contains($openApi, $uri)) { + $failures[] = $uri.' is canonical but missing from OpenAPI.'; + } + if ($status === 'deprecated' && ! str_contains($deprecations, $uri)) { + $failures[] = $uri.' is deprecated but missing from docs/deprecations.md.'; + } + } + + foreach ($failures as $failure) { + $this->error($failure); + } + + return $failures === [] ? self::SUCCESS : self::FAILURE; + } +} diff --git a/app/Console/Commands/GenerateApiRouteInventoryCommand.php b/app/Console/Commands/GenerateApiRouteInventoryCommand.php new file mode 100644 index 0000000..30490ed --- /dev/null +++ b/app/Console/Commands/GenerateApiRouteInventoryCommand.php @@ -0,0 +1,58 @@ +filter(fn (Route $route) => str_starts_with($route->uri(), 'api/')) + ->map(function (Route $route) use ($classification): array { + $action = $route->getActionName(); + [$controller, $method] = str_contains($action, '@') ? explode('@', $action, 2) : [$action, '__invoke']; + $meta = $classification[$controller] ?? []; + + return [ + 'methods' => array_values(array_diff($route->methods(), ['HEAD'])), + 'uri' => $route->uri(), + 'name' => $route->getName(), + 'controller' => $controller, + 'action' => $method, + 'middleware' => $route->gatherMiddleware(), + 'module' => $meta['module'] ?? 'unknown', + 'risk' => $meta['risk'] ?? 'unknown', + 'canonical_status' => $meta['status'] ?? config('api_controller_classification.default_status', 'unknown'), + 'contract' => $meta['contract'] ?? null, + 'auth_required' => collect($route->gatherMiddleware())->contains(fn ($m) => str_starts_with((string) $m, 'auth')), + ]; + }) + ->values() + ->all(); + + Storage::disk('local')->put('generated/api-route-inventory.json', json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + if ($this->option('markdown')) { + $markdown = ["# API Route Inventory", "", "| Method | URI | Name | Controller | Action | Module | Status | Risk | Auth |", "|---|---|---|---|---|---|---|---|---|"]; + foreach ($rows as $row) { + $markdown[] = sprintf('| %s | `%s` | `%s` | `%s` | `%s` | %s | %s | %s | %s |', + implode(',', $row['methods']), $row['uri'], $row['name'] ?? '', $row['controller'], $row['action'], $row['module'], $row['canonical_status'], $row['risk'], $row['auth_required'] ? 'yes' : 'no' + ); + } + @mkdir(base_path('docs'), 0777, true); + file_put_contents(base_path('docs/api-route-inventory.md'), implode(PHP_EOL, $markdown).PHP_EOL); + } + + $this->info('API route inventory written. Unknown routes are now visible, the bare minimum for civilization.'); + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/LegacyUnsafeRouteAuditCommand.php b/app/Console/Commands/LegacyUnsafeRouteAuditCommand.php new file mode 100644 index 0000000..39b8bad --- /dev/null +++ b/app/Console/Commands/LegacyUnsafeRouteAuditCommand.php @@ -0,0 +1,32 @@ + now()->toIso8601String(), + 'items' => array_map(fn (string $key): array => [ + 'key' => $key, + 'status' => 'requires_project_mapping', + 'note' => 'Map this key to actual legacy route names in the real app and disable/remove before production readiness signoff.', + ], $items), + ]; + + $path = storage_path('app/generated/legacy-unsafe-route-audit.json'); + File::ensureDirectoryExists(dirname($path)); + File::put($path, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $this->line(json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/ReleaseFeatureFlagsCommand.php b/app/Console/Commands/ReleaseFeatureFlagsCommand.php new file mode 100644 index 0000000..dedf2bd --- /dev/null +++ b/app/Console/Commands/ReleaseFeatureFlagsCommand.php @@ -0,0 +1,26 @@ + $registry->export()]; + $path = storage_path('app/generated/release-feature-flags.json'); + File::ensureDirectoryExists(dirname($path)); + File::put($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + $this->info('Feature flag rollout map written to '.$path); + $this->line(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/ReleaseMigrationValidateCommand.php b/app/Console/Commands/ReleaseMigrationValidateCommand.php new file mode 100644 index 0000000..cd10d7e --- /dev/null +++ b/app/Console/Commands/ReleaseMigrationValidateCommand.php @@ -0,0 +1,32 @@ + $result->toArray(), $runner->runAll()); + $summary = [ + 'generated_at' => now()->toIso8601String(), + 'results' => $results, + 'status' => collect($results)->every(fn ($result): bool => $result['status'] === 'pass') ? 'pass' : 'fail', + ]; + + $path = storage_path('app/generated/release-validation/validation-summary.json'); + File::ensureDirectoryExists(dirname($path)); + File::put($path, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + $this->info('Migration validation summary written to '.$path); + $this->line(json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return $summary['status'] === 'pass' ? self::SUCCESS : self::FAILURE; + } +} diff --git a/app/Console/Commands/ReleaseReadinessGateCommand.php b/app/Console/Commands/ReleaseReadinessGateCommand.php new file mode 100644 index 0000000..71caf59 --- /dev/null +++ b/app/Console/Commands/ReleaseReadinessGateCommand.php @@ -0,0 +1,26 @@ +evaluate(); + $this->line(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + if ($result['status'] !== 'pass') { + $this->warn('Release readiness gate failed. Fix artifacts or run in warning mode only for early rollout rehearsals.'); + return $this->option('warning-mode') ? self::SUCCESS : self::FAILURE; + } + + $this->info('Release readiness gate passed. Disturbingly competent.'); + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/ReleaseShadowCompareCommand.php b/app/Console/Commands/ReleaseShadowCompareCommand.php new file mode 100644 index 0000000..9c06157 --- /dev/null +++ b/app/Console/Commands/ReleaseShadowCompareCommand.php @@ -0,0 +1,32 @@ + $result->toArray(), $runner->runAll()); + $summary = [ + 'generated_at' => now()->toIso8601String(), + 'results' => $results, + 'status' => collect($results)->contains(fn ($result): bool => $result['status'] === 'fail') ? 'fail' : 'warning', + ]; + + $path = storage_path('app/generated/shadow-comparisons/shadow-summary.json'); + File::ensureDirectoryExists(dirname($path)); + File::put($path, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + $this->warn('Shadow comparisons are scaffolded. Wire legacy comparisons before production cutover.'); + $this->line(json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/RouteInventoryCheckCommand.php b/app/Console/Commands/RouteInventoryCheckCommand.php new file mode 100644 index 0000000..7145c34 --- /dev/null +++ b/app/Console/Commands/RouteInventoryCheckCommand.php @@ -0,0 +1,43 @@ +option('path')); + if (! is_file($path)) { + $this->error('Route inventory missing. Run php artisan api:route-inventory first.'); + return self::FAILURE; + } + + $rows = json_decode(file_get_contents($path) ?: '[]', true) ?: []; + $failures = []; + foreach ($rows as $row) { + $uri = (string) ($row['uri'] ?? ''); + $methods = $row['methods'] ?? []; + $mutation = count(array_intersect($methods, ['POST', 'PUT', 'PATCH', 'DELETE'])) > 0; + if (($row['module'] ?? 'unknown') === 'unknown') { + $failures[] = $uri.' is missing module classification.'; + } + if ($mutation && empty($row['auth_required'])) { + $failures[] = $uri.' is a mutation route without auth metadata.'; + } + if (str_contains($uri, 'islamic-sunday-school') && ! str_contains(json_encode($row['middleware'] ?? []), 'domain.profile')) { + $failures[] = $uri.' is an extension route without domain profile middleware.'; + } + } + + foreach ($failures as $failure) { + $this->error($failure); + } + + return $failures === [] ? self::SUCCESS : self::FAILURE; + } +} diff --git a/app/Domain/IslamicSundaySchool/Attendance/IslamicSundaySchoolAttendancePolicy.php b/app/Domain/IslamicSundaySchool/Attendance/IslamicSundaySchoolAttendancePolicy.php new file mode 100644 index 0000000..27ab73c --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/IslamicSundaySchoolAttendancePolicy.php @@ -0,0 +1,32 @@ +format('N') === 7; + } + + public function determineStatus(SchoolContext $context, DateTimeInterface $scanTime, array $session): string + { + $startsAt = $session['starts_at'] ?? null; + if ($startsAt instanceof DateTimeInterface && $scanTime > $startsAt) { + return 'late'; + } + + return 'present'; + } + + public function duplicateScanWindowSeconds(SchoolContext $context): int + { + return 600; + } +} diff --git a/app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolAttendancePolicy.php b/app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolAttendancePolicy.php new file mode 100644 index 0000000..981a306 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolAttendancePolicy.php @@ -0,0 +1,16 @@ +startsAt->modify('+15 minutes'); + $status = $timestamp > $lateThreshold ? 'late' : 'present'; + + return new AttendanceStatusResult($status, $status === 'late', $status === 'late' ? 'Configured extension late window.' : null); + } +} diff --git a/app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolSessionPolicy.php b/app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolSessionPolicy.php new file mode 100644 index 0000000..3f02f24 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/Policies/IslamicSundaySchoolSessionPolicy.php @@ -0,0 +1,13 @@ +app->bind(AttendancePolicyContract::class, IslamicSundaySchoolAttendancePolicy::class); + $this->app->bind(AttendanceStatusPolicyContract::class, IslamicSundaySchoolAttendanceStatusPolicy::class); + $this->app->bind(AttendanceSessionResolverContract::class, IslamicSundaySchoolSessionResolver::class); + } +} diff --git a/app/Domain/IslamicSundaySchool/Attendance/README.md b/app/Domain/IslamicSundaySchool/Attendance/README.md new file mode 100644 index 0000000..d3a2cb7 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/README.md @@ -0,0 +1,3 @@ +# Attendance + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Attendance/Reports/IslamicSundaySchoolAttendanceReportBuilder.php b/app/Domain/IslamicSundaySchool/Attendance/Reports/IslamicSundaySchoolAttendanceReportBuilder.php new file mode 100644 index 0000000..448c006 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/Reports/IslamicSundaySchoolAttendanceReportBuilder.php @@ -0,0 +1,17 @@ + 'Present', + 'late' => 'Late', + 'absent' => 'Absent', + ]; + } +} diff --git a/app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolLateArrivalEvaluator.php b/app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolLateArrivalEvaluator.php new file mode 100644 index 0000000..c47e7a7 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolLateArrivalEvaluator.php @@ -0,0 +1,16 @@ + $session->startsAt->modify('+15 minutes'); + } +} diff --git a/app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolSessionResolver.php b/app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolSessionResolver.php new file mode 100644 index 0000000..fbdd763 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Attendance/Services/IslamicSundaySchoolSessionResolver.php @@ -0,0 +1,22 @@ +fallback->resolveForScan($context, $subject, $timestamp); + } +} diff --git a/app/Domain/IslamicSundaySchool/Communication/Policies/IslamicSundaySchoolCommunicationPreferencePolicy.php b/app/Domain/IslamicSundaySchool/Communication/Policies/IslamicSundaySchoolCommunicationPreferencePolicy.php new file mode 100644 index 0000000..61c1263 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Policies/IslamicSundaySchoolCommunicationPreferencePolicy.php @@ -0,0 +1 @@ +app->tag([SundayClassRecipientResolver::class,VolunteerTeamRecipientResolver::class,MasjidFamilyRecipientResolver::class,VolunteerRecipientResolver::class,IslamicSundaySchoolProgramRecipientResolver::class], 'communication.recipient_resolvers'); } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Communication/Resolvers/IslamicSundaySchoolProgramRecipientResolver.php b/app/Domain/IslamicSundaySchool/Communication/Resolvers/IslamicSundaySchoolProgramRecipientResolver.php new file mode 100644 index 0000000..51fff3e --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Resolvers/IslamicSundaySchoolProgramRecipientResolver.php @@ -0,0 +1 @@ +audienceType === 'program_attendees'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian',$context->schoolId*2000+1,'Extension Recipient','program_attendees@example.test','+15555550200',$query->language,[],null,null,$context->schoolId,[$query->channel->value],[],['extension'=>'islamic_sunday_school'],['program_attendees'])]); } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Communication/Resolvers/MasjidFamilyRecipientResolver.php b/app/Domain/IslamicSundaySchool/Communication/Resolvers/MasjidFamilyRecipientResolver.php new file mode 100644 index 0000000..1638ffa --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Resolvers/MasjidFamilyRecipientResolver.php @@ -0,0 +1 @@ +audienceType === 'masjid_family'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian',$context->schoolId*2000+1,'Extension Recipient','masjid_family@example.test','+15555550200',$query->language,[],null,null,$context->schoolId,[$query->channel->value],[],['extension'=>'islamic_sunday_school'],['masjid_family'])]); } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Communication/Resolvers/SundayClassRecipientResolver.php b/app/Domain/IslamicSundaySchool/Communication/Resolvers/SundayClassRecipientResolver.php new file mode 100644 index 0000000..261d684 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Resolvers/SundayClassRecipientResolver.php @@ -0,0 +1 @@ +audienceType === 'sunday_class'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian',$context->schoolId*2000+1,'Extension Recipient','sunday_class@example.test','+15555550200',$query->language,[],null,null,$context->schoolId,[$query->channel->value],[],['extension'=>'islamic_sunday_school'],['sunday_class'])]); } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerRecipientResolver.php b/app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerRecipientResolver.php new file mode 100644 index 0000000..610a4ad --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerRecipientResolver.php @@ -0,0 +1 @@ +audienceType === 'volunteers'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian',$context->schoolId*2000+1,'Extension Recipient','volunteers@example.test','+15555550200',$query->language,[],null,null,$context->schoolId,[$query->channel->value],[],['extension'=>'islamic_sunday_school'],['volunteers'])]); } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerTeamRecipientResolver.php b/app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerTeamRecipientResolver.php new file mode 100644 index 0000000..49df44d --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Resolvers/VolunteerTeamRecipientResolver.php @@ -0,0 +1 @@ +audienceType === 'volunteer_team'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian',$context->schoolId*2000+1,'Extension Recipient','volunteer_team@example.test','+15555550200',$query->language,[],null,null,$context->schoolId,[$query->channel->value],[],['extension'=>'islamic_sunday_school'],['volunteer_team'])]); } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Communication/Templates/IslamicSundaySchoolTemplateCatalog.php b/app/Domain/IslamicSundaySchool/Communication/Templates/IslamicSundaySchoolTemplateCatalog.php new file mode 100644 index 0000000..33e17a6 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Communication/Templates/IslamicSundaySchoolTemplateCatalog.php @@ -0,0 +1 @@ +['category'=>'event'], 'volunteer_reminder'=>['category'=>'event'], 'attendance_follow_up'=>['category'=>'attendance']]; } } \ No newline at end of file diff --git a/app/Domain/IslamicSundaySchool/Contracts/README.md b/app/Domain/IslamicSundaySchool/Contracts/README.md new file mode 100644 index 0000000..5b5e481 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Contracts/README.md @@ -0,0 +1,3 @@ +# Contracts + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Curriculum/README.md b/app/Domain/IslamicSundaySchool/Curriculum/README.md new file mode 100644 index 0000000..87ee7eb --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Curriculum/README.md @@ -0,0 +1,3 @@ +# Curriculum + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Families/README.md b/app/Domain/IslamicSundaySchool/Families/README.md new file mode 100644 index 0000000..7f84081 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Families/README.md @@ -0,0 +1,3 @@ +# Families + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php b/app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php new file mode 100644 index 0000000..cd1d666 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php @@ -0,0 +1,17 @@ +$invoiceView['id'] ?? null,'amount_minor'=>$paymentAmount->minorAmount(),'currency'=>$paymentAmount->currency(),'strategy'=>'extension_policy_family_program_order']]; + } +} diff --git a/app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolTuitionPolicy.php b/app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolTuitionPolicy.php new file mode 100644 index 0000000..b27809a --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Finance/Policies/IslamicSundaySchoolTuitionPolicy.php @@ -0,0 +1,16 @@ +$context->domainProfile,'charges'=>[],'labels_are_extension_owned'=>true]; + } +} diff --git a/app/Domain/IslamicSundaySchool/Finance/Providers/IslamicSundaySchoolFinanceServiceProvider.php b/app/Domain/IslamicSundaySchool/Finance/Providers/IslamicSundaySchoolFinanceServiceProvider.php new file mode 100644 index 0000000..ebc3865 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Finance/Providers/IslamicSundaySchoolFinanceServiceProvider.php @@ -0,0 +1,21 @@ +app->bind(TuitionPolicyContract::class, IslamicSundaySchoolTuitionPolicy::class); + $this->app->bind(PaymentAllocationPolicyContract::class, IslamicSundaySchoolPaymentAllocationPolicy::class); + } +} diff --git a/app/Domain/IslamicSundaySchool/Finance/README.md b/app/Domain/IslamicSundaySchool/Finance/README.md new file mode 100644 index 0000000..bca288a --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Finance/README.md @@ -0,0 +1,3 @@ +# Islamic Sunday School Finance Extension + +Owns extension labels, tuition policies, allocation policies, scholarships, and local reports. It depends on SchoolCore Finance contracts. SchoolCore Finance must not depend back on this namespace. diff --git a/app/Domain/IslamicSundaySchool/Finance/Reports/IslamicSundaySchoolFinanceReportBuilder.php b/app/Domain/IslamicSundaySchool/Finance/Reports/IslamicSundaySchoolFinanceReportBuilder.php new file mode 100644 index 0000000..d53cdaf --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Finance/Reports/IslamicSundaySchoolFinanceReportBuilder.php @@ -0,0 +1,17 @@ +$context->domainProfile,'rows'=>$this->finance->outstandingInvoices($context)]; + } +} diff --git a/app/Domain/IslamicSundaySchool/Ministry/README.md b/app/Domain/IslamicSundaySchool/Ministry/README.md new file mode 100644 index 0000000..f6185ce --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Ministry/README.md @@ -0,0 +1,3 @@ +# Ministry + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php b/app/Domain/IslamicSundaySchool/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php new file mode 100644 index 0000000..2739c3c --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Policies/IslamicSundaySchoolPaymentAllocationPolicy.php @@ -0,0 +1,16 @@ + $invoiceId, 'amount' => $amount, 'allocation_rule' => 'extension_default']]; + } +} diff --git a/app/Domain/IslamicSundaySchool/Policies/README.md b/app/Domain/IslamicSundaySchool/Policies/README.md new file mode 100644 index 0000000..462f314 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Policies/README.md @@ -0,0 +1,3 @@ +# Policies + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Providers/IslamicSundaySchoolCalendarProvider.php b/app/Domain/IslamicSundaySchool/Providers/IslamicSundaySchoolCalendarProvider.php new file mode 100644 index 0000000..0054171 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Providers/IslamicSundaySchoolCalendarProvider.php @@ -0,0 +1,27 @@ +schoolYear) ? (int) $context->schoolYear : null; + } + + public function currentTerm(SchoolContext $context): ?int + { + return is_numeric($context->term) ? (int) $context->term : null; + } + + public function sessionsForDate(SchoolContext $context, DateTimeInterface $date): array + { + return []; + } +} diff --git a/app/Domain/IslamicSundaySchool/Providers/README.md b/app/Domain/IslamicSundaySchool/Providers/README.md new file mode 100644 index 0000000..e3b5e16 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Providers/README.md @@ -0,0 +1,3 @@ +# Providers + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Reporting/ExportTemplates/FamilySummaryExportTemplate.php b/app/Domain/IslamicSundaySchool/Reporting/ExportTemplates/FamilySummaryExportTemplate.php new file mode 100644 index 0000000..56fe118 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/ExportTemplates/FamilySummaryExportTemplate.php @@ -0,0 +1,3 @@ +app->tag([ + QuranProgressReport::class, + ArabicLevelReport::class, + IslamicStudiesProgressReport::class, + HalaqaAttendanceReport::class, + SundayProgramAttendanceReport::class, + MasjidFamilySummaryReport::class, + VolunteerTeamReport::class, + IslamicSundaySchoolDashboardReport::class, + ], 'schoolcore.reports'); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/ReadModels/ArabicProgressReadModel.php b/app/Domain/IslamicSundaySchool/Reporting/ReadModels/ArabicProgressReadModel.php new file mode 100644 index 0000000..16bffaf --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/ReadModels/ArabicProgressReadModel.php @@ -0,0 +1,4 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/BaseIslamicSundaySchoolReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/BaseIslamicSundaySchoolReport.php new file mode 100644 index 0000000..5d42f19 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/BaseIslamicSundaySchoolReport.php @@ -0,0 +1,12 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicStudiesProgressReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicStudiesProgressReport.php new file mode 100644 index 0000000..2649bc8 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicStudiesProgressReport.php @@ -0,0 +1,21 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicSundaySchoolDashboardReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicSundaySchoolDashboardReport.php new file mode 100644 index 0000000..ff61561 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/IslamicSundaySchoolDashboardReport.php @@ -0,0 +1,21 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/MasjidFamilySummaryReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/MasjidFamilySummaryReport.php new file mode 100644 index 0000000..dc19a9c --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/MasjidFamilySummaryReport.php @@ -0,0 +1,21 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/QuranProgressReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/QuranProgressReport.php new file mode 100644 index 0000000..f442e38 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/QuranProgressReport.php @@ -0,0 +1,21 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/SundayProgramAttendanceReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/SundayProgramAttendanceReport.php new file mode 100644 index 0000000..206050c --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/SundayProgramAttendanceReport.php @@ -0,0 +1,21 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reporting/Reports/VolunteerTeamReport.php b/app/Domain/IslamicSundaySchool/Reporting/Reports/VolunteerTeamReport.php new file mode 100644 index 0000000..f012f09 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reporting/Reports/VolunteerTeamReport.php @@ -0,0 +1,21 @@ + 'note', 'label' => 'Note', 'sensitive' => false]]; } + public function sensitiveColumns(): array { return ['sensitive_notes']; } + public function run(SchoolContext $context, array $filters = []): ReportResultData + { + return new ReportResultData($this->key(), $this->columns(), [[ + 'school_id' => $context->schoolId, + 'report_key' => $this->key(), + 'note' => 'Extension report scaffold. Wire this to Islamic Sunday School read models.', + ]], null, null, null, 1, gmdate('c')); + } +} diff --git a/app/Domain/IslamicSundaySchool/Reports/README.md b/app/Domain/IslamicSundaySchool/Reports/README.md new file mode 100644 index 0000000..7b00b54 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Reports/README.md @@ -0,0 +1,3 @@ +# Reports + +Extension module. Bind policies/providers here without leaking implementation details into SchoolCore. diff --git a/app/Domain/IslamicSundaySchool/Students/DTO/IslamicSundaySchoolStudentProfileData.php b/app/Domain/IslamicSundaySchool/Students/DTO/IslamicSundaySchoolStudentProfileData.php new file mode 100644 index 0000000..5079c70 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/DTO/IslamicSundaySchoolStudentProfileData.php @@ -0,0 +1,4 @@ +isIslamicSundaySchool() && $data->studentId>0; } } diff --git a/app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolPromotionPolicy.php b/app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolPromotionPolicy.php new file mode 100644 index 0000000..162a2f6 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolPromotionPolicy.php @@ -0,0 +1,5 @@ +isIslamicSundaySchool() && $student->status==='active',$student->status==='active'?'promoted':'manual_review'); } } diff --git a/app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolStudentIdentifierGenerator.php b/app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolStudentIdentifierGenerator.php new file mode 100644 index 0000000..05b23b3 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/Policies/IslamicSundaySchoolStudentIdentifierGenerator.php @@ -0,0 +1,5 @@ +id)); } } diff --git a/app/Domain/IslamicSundaySchool/Students/Providers/IslamicSundaySchoolStudentServiceProvider.php b/app/Domain/IslamicSundaySchool/Students/Providers/IslamicSundaySchoolStudentServiceProvider.php new file mode 100644 index 0000000..aed927f --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/Providers/IslamicSundaySchoolStudentServiceProvider.php @@ -0,0 +1,5 @@ +app->bind(StudentIdentifierGeneratorContract::class,IslamicSundaySchoolStudentIdentifierGenerator::class); $this->app->bind(PromotionPolicyContract::class,IslamicSundaySchoolPromotionPolicy::class); } } } diff --git a/app/Domain/IslamicSundaySchool/Students/Reports/IslamicSundaySchoolStudentReportBuilder.php b/app/Domain/IslamicSundaySchool/Students/Reports/IslamicSundaySchoolStudentReportBuilder.php new file mode 100644 index 0000000..c0f1ff0 --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/Reports/IslamicSundaySchoolStudentReportBuilder.php @@ -0,0 +1,5 @@ +$context->domainProfile,'filters'=>$filters,'labels'=>['quran_level','arabic_level','islamic_studies_track']]; } } diff --git a/app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolFamilyProfileService.php b/app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolFamilyProfileService.php new file mode 100644 index 0000000..0f0789e --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolFamilyProfileService.php @@ -0,0 +1,5 @@ +$context->schoolId,'student_id'=>$studentId,'extension'=>'islamic_sunday_school']; } } diff --git a/app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolStudentProfileService.php b/app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolStudentProfileService.php new file mode 100644 index 0000000..37711ba --- /dev/null +++ b/app/Domain/IslamicSundaySchool/Students/Services/IslamicSundaySchoolStudentProfileService.php @@ -0,0 +1,5 @@ +updateOrInsert(['school_id'=>$context->schoolId,'student_id'=>$data->studentId],['family_profile_id'=>$data->familyProfileId,'quran_level_id'=>$data->quranLevelId,'arabic_level_id'=>$data->arabicLevelId,'islamic_studies_track_id'=>$data->islamicStudiesTrackId,'halaqa_group_id'=>$data->halaqaGroupId,'memorization_progress_summary'=>$data->memorizationProgressSummary,'recitation_placement'=>$data->recitationPlacement,'volunteer_family_participation'=>$data->volunteerFamilyParticipation,'sensitive_admin_notes'=>$data->sensitiveAdminNotes,'metadata'=>json_encode($data->metadata),'updated_at'=>now()]); return ['student_id'=>$data->studentId,'profile'=>'islamic_sunday_school']; } } diff --git a/app/Domain/README.md b/app/Domain/README.md new file mode 100644 index 0000000..9145860 --- /dev/null +++ b/app/Domain/README.md @@ -0,0 +1,24 @@ +# Domain Boundary Rules + +Dependency direction: + +```text +Http / Controllers / Requests / Resources + ↓ +Application Services / Use Cases + ↓ +SchoolCore Contracts + ↓ +SchoolCore Implementations + ↑ +Domain Extensions may provide policies/providers +``` + +Rules: + +- `SchoolCore` defines neutral platform capabilities. +- Extension modules may depend on `SchoolCore` contracts. +- `SchoolCore` must not import extension namespaces. +- Domain services must not accept `Illuminate\Http\Request`. +- Domain services must not call `auth()`. +- Controllers should depend on contracts, not concrete domain services. diff --git a/app/Domain/SchoolCore/Academics/Policies/DefaultGradeScalePolicy.php b/app/Domain/SchoolCore/Academics/Policies/DefaultGradeScalePolicy.php new file mode 100644 index 0000000..933cf05 --- /dev/null +++ b/app/Domain/SchoolCore/Academics/Policies/DefaultGradeScalePolicy.php @@ -0,0 +1,17 @@ += 90 => 'A', $score >= 80 => 'B', $score >= 70 => 'C', $score >= 60 => 'D', default => 'F' }; + } + public function passingScore(SchoolContext $context): float { return 60.0; } +} diff --git a/app/Domain/SchoolCore/Academics/Policies/DefaultPromotionPolicy.php b/app/Domain/SchoolCore/Academics/Policies/DefaultPromotionPolicy.php new file mode 100644 index 0000000..f857ac7 --- /dev/null +++ b/app/Domain/SchoolCore/Academics/Policies/DefaultPromotionPolicy.php @@ -0,0 +1,14 @@ + $this->accepted, + 'status' => $this->status, + 'message' => $this->message, + 'subject' => $this->subject ? [ + 'type' => $this->subject->subjectType, + 'id' => $this->subject->subjectId, + 'name' => $this->subject->displayName, + ] : null, + 'session' => $this->session ? [ + 'id' => $this->session->id, + 'name' => $this->session->name, + 'type' => $this->session->sessionType, + ] : null, + 'attendance_record_id' => $this->attendanceRecordId, + 'duplicate' => $this->duplicate, + 'payload' => $this->payload, + ]; + } +} diff --git a/app/Domain/SchoolCore/Attendance/Data/AttendanceMarkData.php b/app/Domain/SchoolCore/Attendance/Data/AttendanceMarkData.php new file mode 100644 index 0000000..e034ad4 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Data/AttendanceMarkData.php @@ -0,0 +1,12 @@ + $details */ + public function __construct(public bool $accepted, public string $status, public array $details = []) {} +} diff --git a/app/Domain/SchoolCore/Attendance/Data/AttendanceScanData.php b/app/Domain/SchoolCore/Attendance/Data/AttendanceScanData.php new file mode 100644 index 0000000..a482bec --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Data/AttendanceScanData.php @@ -0,0 +1,12 @@ +schoolId === $context->schoolId && $session->schoolId === $context->schoolId && $subject->active; + } + + public function duplicateScanWindowSeconds(SchoolContext $context): int + { + return 90; + } + + public function allowCheckout(SchoolContext $context): bool + { + return true; + } + + public function allowManualOverride(SchoolContext $context): bool + { + return true; + } +} diff --git a/app/Domain/SchoolCore/Attendance/Policies/DefaultAttendanceSessionPolicy.php b/app/Domain/SchoolCore/Attendance/Policies/DefaultAttendanceSessionPolicy.php new file mode 100644 index 0000000..998d0dd --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Policies/DefaultAttendanceSessionPolicy.php @@ -0,0 +1,13 @@ +startsAt->modify('+10 minutes'); + $status = $timestamp > $lateThreshold ? 'late' : 'present'; + + return new AttendanceStatusResult($status, $status === 'late'); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Policies/StandardSchoolAttendancePolicy.php b/app/Domain/SchoolCore/Attendance/Policies/StandardSchoolAttendancePolicy.php new file mode 100644 index 0000000..33745d0 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Policies/StandardSchoolAttendancePolicy.php @@ -0,0 +1,32 @@ +format('N'), [6, 7], true); + } + + public function determineStatus(SchoolContext $context, DateTimeInterface $scanTime, array $session): string + { + $startsAt = $session['starts_at'] ?? null; + if (! $startsAt instanceof DateTimeInterface) { + return 'present'; + } + + return $scanTime <= $startsAt ? 'present' : 'late'; + } + + public function duplicateScanWindowSeconds(SchoolContext $context): int + { + return 300; + } +} diff --git a/app/Domain/SchoolCore/Attendance/Providers/StandardAcademicCalendarProvider.php b/app/Domain/SchoolCore/Attendance/Providers/StandardAcademicCalendarProvider.php new file mode 100644 index 0000000..55de7aa --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Providers/StandardAcademicCalendarProvider.php @@ -0,0 +1,27 @@ +schoolYear) ? (int) $context->schoolYear : null; + } + + public function currentTerm(SchoolContext $context): ?int + { + return is_numeric($context->term) ? (int) $context->term : null; + } + + public function sessionsForDate(SchoolContext $context, DateTimeInterface $date): array + { + return []; + } +} diff --git a/app/Domain/SchoolCore/Attendance/README.md b/app/Domain/SchoolCore/Attendance/README.md new file mode 100644 index 0000000..1aa12f0 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/README.md @@ -0,0 +1,12 @@ +# SchoolCore Attendance + +Phase 4 extracts scanner and attendance behavior into neutral, context-aware services. + +Rules: +- every public mutation accepts `SchoolContext`; +- controllers validate and delegate only; +- scanner writes go through `ScannerServiceContract`; +- badge lookup is school-scoped; +- duplicate scans use idempotency/fingerprints; +- status and session rules are policy/provider driven; +- `SchoolCore\Attendance` must not import `IslamicSundaySchool`. diff --git a/app/Domain/SchoolCore/Attendance/Repositories/AttendanceReadRepository.php b/app/Domain/SchoolCore/Attendance/Repositories/AttendanceReadRepository.php new file mode 100644 index 0000000..f23c2a1 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Repositories/AttendanceReadRepository.php @@ -0,0 +1,32 @@ +where('school_id', $context->schoolId); + + if ($query->sessionId !== null) { + $builder->where('attendance_session_id', $query->sessionId); + } + if ($query->fromDate !== null) { + $builder->where('attendance_date', '>=', $query->fromDate); + } + if ($query->toDate !== null) { + $builder->where('attendance_date', '<=', $query->toDate); + } + if ($query->subjectType !== null) { + $builder->where('subject_type', $query->subjectType); + } + + return $builder->selectRaw('status, count(*) as total')->groupBy('status')->pluck('total', 'status')->all(); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Repositories/AttendanceRepository.php b/app/Domain/SchoolCore/Attendance/Repositories/AttendanceRepository.php new file mode 100644 index 0000000..fe606a6 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Repositories/AttendanceRepository.php @@ -0,0 +1,45 @@ +where('school_id', $context->schoolId); + + foreach ($attributes as $column => $value) { + $query->where($column, $value); + } + + $existing = $query->lockForUpdate()->first(); + + if ($existing) { + DB::table('attendance_records')->where('id', $existing->id)->update($values + ['updated_at' => now()]); + return DB::table('attendance_records')->where('id', $existing->id)->first(); + } + + $id = DB::table('attendance_records')->insertGetId($attributes + $values + [ + 'school_id' => $context->schoolId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return DB::table('attendance_records')->where('id', $id)->first(); + } + + public function createScanLog(SchoolContext $context, array $values): int + { + return (int) DB::table('attendance_scan_logs')->insertGetId($values + [ + 'school_id' => $context->schoolId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Repositories/AttendanceSessionRepository.php b/app/Domain/SchoolCore/Attendance/Repositories/AttendanceSessionRepository.php new file mode 100644 index 0000000..047f5cd --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Repositories/AttendanceSessionRepository.php @@ -0,0 +1,22 @@ +where('school_id', $context->schoolId) + ->where('starts_at', '<=', $timestamp->format('Y-m-d H:i:s')) + ->where('ends_at', '>=', $timestamp->format('Y-m-d H:i:s')) + ->where('status', 'open') + ->orderBy('starts_at') + ->first(); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Repositories/BadgeRepository.php b/app/Domain/SchoolCore/Attendance/Repositories/BadgeRepository.php new file mode 100644 index 0000000..08d2f56 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Repositories/BadgeRepository.php @@ -0,0 +1,37 @@ +where('school_id', $context->schoolId) + ->where('badge_value', $data->badgeValue) + ->where('active', true) + ->first(); + + if (! $badge) { + throw new BadgeNotFoundException('Badge was not found for this school.'); + } + + return new BadgeLookupResult(new AttendanceSubjectView( + subjectType: (string) $badge->subject_type, + subjectId: (int) $badge->subject_id, + schoolId: (int) $badge->school_id, + displayName: (string) ($badge->display_name ?? ucfirst((string) $badge->subject_type).' #'.$badge->subject_id), + active: (bool) $badge->active, + metadata: ['badge_value' => $data->badgeValue], + ), (int) $badge->id); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/AttendanceAuditLogger.php b/app/Domain/SchoolCore/Attendance/Services/AttendanceAuditLogger.php new file mode 100644 index 0000000..0ad7277 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/AttendanceAuditLogger.php @@ -0,0 +1,39 @@ +insert([ + 'school_id' => $context->schoolId, + 'academic_year_id' => is_numeric($context->schoolYear) ? (int) $context->schoolYear : null, + 'term_id' => is_numeric($context->term) ? (int) $context->term : null, + 'actor_user_id' => $context->actorUserId, + 'actor_type' => 'user', + 'action' => $data->action, + 'subject_type' => $data->subjectType, + 'subject_id' => $data->subjectId, + 'attendance_session_id' => $data->attendanceSessionId, + 'attendance_record_id' => $data->attendanceRecordId, + 'before_json' => $data->before ? json_encode($data->before, JSON_THROW_ON_ERROR) : null, + 'after_json' => $data->after ? json_encode($data->after, JSON_THROW_ON_ERROR) : null, + 'reason' => $data->reason, + 'source' => $data->source, + 'station_id' => $data->stationId, + 'request_id' => $data->requestId, + 'idempotency_key' => $data->idempotencyKey, + 'metadata_json' => json_encode($data->metadata, JSON_THROW_ON_ERROR), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/AttendanceIdempotencyService.php b/app/Domain/SchoolCore/Attendance/Services/AttendanceIdempotencyService.php new file mode 100644 index 0000000..8402e8d --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/AttendanceIdempotencyService.php @@ -0,0 +1,59 @@ +where('school_id', $context->schoolId) + ->where('operation', $data->operation) + ->where('idempotency_key', $data->key) + ->first(); + + if (! $row) { + return null; + } + + if ((string) $row->payload_hash !== $data->payloadHash) { + throw new DuplicateScanException('Same attendance idempotency key was reused with a different payload.'); + } + + $payload = json_decode((string) $row->result_json, true) ?: []; + + return new ScanResult( + accepted: (bool) ($payload['accepted'] ?? true), + status: (string) ($payload['status'] ?? 'duplicate'), + message: (string) ($payload['message'] ?? 'Duplicate scan returned prior result.'), + duplicate: true, + payload: $payload, + ); + } + + public function storeResult(SchoolContext $context, AttendanceIdempotencyData $data, ProcessScanData $scan, ScanResult $result): void + { + DB::table('attendance_idempotency_keys')->updateOrInsert([ + 'school_id' => $context->schoolId, + 'operation' => $data->operation, + 'idempotency_key' => $data->key, + ], [ + 'payload_hash' => $data->payloadHash, + 'fingerprint' => $data->fingerprint, + 'result_json' => json_encode($result->toArray(), JSON_THROW_ON_ERROR), + 'expires_at' => now()->addDay(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/AttendanceService.php b/app/Domain/SchoolCore/Attendance/Services/AttendanceService.php new file mode 100644 index 0000000..2ee825b --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/AttendanceService.php @@ -0,0 +1,44 @@ +where('school_id', $context->schoolId)->whereKey($sessionId)->first(); + + if (! $session) { + throw new \RuntimeException('Attendance session not found.'); + } + + return new AttendanceSessionView( + id: (int) $session->id, + schoolId: (int) $session->school_id, + name: (string) $session->name, + sessionType: (string) $session->session_type, + startsAt: new DateTimeImmutable((string) $session->starts_at), + endsAt: new DateTimeImmutable((string) $session->ends_at), + timezone: (string) ($session->timezone ?? $context->timezone), + status: (string) $session->status, + ); + } + + public function summarizeAttendance(SchoolContext $context, AttendanceSummaryQuery $query): AttendanceSummaryResult + { + return new AttendanceSummaryResult($this->reads->summarize($context, $query)); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/AttendanceSessionResolver.php b/app/Domain/SchoolCore/Attendance/Services/AttendanceSessionResolver.php new file mode 100644 index 0000000..7b6d15c --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/AttendanceSessionResolver.php @@ -0,0 +1,40 @@ +sessions->currentForTimestamp($context, $timestamp); + + if (! $session) { + throw new AttendanceSessionNotFoundException('No open attendance session was found for this scan.'); + } + + return new AttendanceSessionView( + id: (int) $session->id, + schoolId: (int) $session->school_id, + name: (string) $session->name, + sessionType: (string) $session->session_type, + startsAt: new DateTimeImmutable((string) $session->starts_at), + endsAt: new DateTimeImmutable((string) $session->ends_at), + timezone: (string) ($session->timezone ?? $context->timezone), + status: (string) $session->status, + metadata: json_decode((string) ($session->metadata ?? '[]'), true) ?: [], + ); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/AttendanceStatusResolver.php b/app/Domain/SchoolCore/Attendance/Services/AttendanceStatusResolver.php new file mode 100644 index 0000000..edae7b3 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/AttendanceStatusResolver.php @@ -0,0 +1,21 @@ +policy->statusForScan($context, $session, $timestamp); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/BadgeLookupService.php b/app/Domain/SchoolCore/Attendance/Services/BadgeLookupService.php new file mode 100644 index 0000000..268bff4 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/BadgeLookupService.php @@ -0,0 +1,21 @@ +badges->lookup($context, $data); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/LateArrivalEvaluator.php b/app/Domain/SchoolCore/Attendance/Services/LateArrivalEvaluator.php new file mode 100644 index 0000000..73285f6 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/LateArrivalEvaluator.php @@ -0,0 +1,16 @@ + $session->startsAt->modify('+'.$graceMinutes.' minutes'); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/ScannerService.php b/app/Domain/SchoolCore/Attendance/Services/ScannerService.php new file mode 100644 index 0000000..55aea0d --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/ScannerService.php @@ -0,0 +1,121 @@ +policy->duplicateScanWindowSeconds($context); + $fingerprint = ScanFingerprint::fromScan($context, $data, $window); + $key = $data->idempotencyKey ?: $data->clientRequestId ?: $fingerprint; + $idem = new AttendanceIdempotencyData('scanner.process', $key, ScanFingerprint::payloadHash($data), $fingerprint); + + if ($existing = $this->idempotency->findExisting($context, $idem)) { + $this->audit->log($context, new AttendanceAuditData( + action: 'attendance.scan.duplicate', + subjectType: 'unknown', + subjectId: 0, + source: $data->scanSource, + stationId: $data->stationId, + idempotencyKey: $key, + )); + return $existing; + } + + $badge = $this->badges->lookup($context, new BadgeLookupData($data->badgeValue, stationId: $data->stationId)); + $session = $this->sessions->resolveForScan($context, $badge->subject, $data->scannedAt); + + if (! $this->policy->canRecordAttendance($context, $badge->subject, $session)) { + throw new UnauthorizedAttendanceAccessException('Actor may not record attendance for this subject/session.'); + } + + $status = $this->statuses->statusForScan($context, $session, $data->scannedAt); + $date = $data->scannedAt->setTimezone(new \DateTimeZone($session->timezone))->format('Y-m-d'); + $record = null; + + if ($badge->subject->subjectType === 'student') { + $record = $this->students->recordStudentAttendance($context, new RecordStudentAttendanceData( + studentId: $badge->subject->subjectId, + sessionId: $session->id, + attendanceDate: $date, + status: $status->status, + source: $data->scanSource, + recordedAt: $data->scannedAt, + idempotencyKey: $key, + metadata: $data->metadata, + )); + } elseif ($badge->subject->subjectType === 'staff' || $badge->subject->subjectType === 'volunteer') { + $record = $this->staff->recordStaffAttendance($context, new RecordStaffAttendanceData( + staffId: $badge->subject->subjectId, + sessionId: $session->id, + attendanceDate: $date, + status: $status->status, + source: $data->scanSource, + recordedAt: $data->scannedAt, + idempotencyKey: $key, + metadata: $data->metadata, + )); + } + + $result = new ScanResult( + accepted: true, + status: $status->status, + message: $status->late ? 'Scan accepted. Subject is late.' : 'Scan accepted.', + subject: $badge->subject, + session: $session, + attendanceRecordId: $record?->recordId, + duplicate: false, + payload: ['late' => $status->late], + ); + + $this->audit->log($context, new AttendanceAuditData( + action: 'attendance.scan.processed', + subjectType: $badge->subject->subjectType, + subjectId: $badge->subject->subjectId, + attendanceSessionId: $session->id, + attendanceRecordId: $record?->recordId, + after: $result->toArray(), + source: $data->scanSource, + stationId: $data->stationId, + idempotencyKey: $key, + )); + + $this->idempotency->storeResult($context, $idem, $data, $result); + + return $result; + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/StaffAttendanceService.php b/app/Domain/SchoolCore/Attendance/Services/StaffAttendanceService.php new file mode 100644 index 0000000..31f8e7c --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/StaffAttendanceService.php @@ -0,0 +1,57 @@ +transactions->run(function () use ($context, $data): AttendanceRecordResult { + $record = $this->records->upsertRecord($context, [ + 'attendance_session_id' => $data->sessionId, + 'attendance_date' => $data->attendanceDate, + 'subject_type' => 'staff', + 'subject_id' => $data->staffId, + ], [ + 'status' => $data->status, + 'source' => $data->source, + 'first_seen_at' => $data->recordedAt->format('Y-m-d H:i:s'), + 'last_seen_at' => $data->recordedAt->format('Y-m-d H:i:s'), + 'recorded_by_user_id' => $context->actorUserId, + 'override_reason' => $data->overrideReason, + 'metadata' => json_encode($data->metadata, JSON_THROW_ON_ERROR), + ]); + + $this->audit->log($context, new AttendanceAuditData( + action: 'attendance.staff.recorded', + subjectType: 'staff', + subjectId: $data->staffId, + attendanceSessionId: $data->sessionId, + attendanceRecordId: (int) $record->id, + after: ['status' => $data->status], + reason: $data->reason ?? $data->overrideReason, + source: $data->source, + idempotencyKey: $data->idempotencyKey, + )); + + return new AttendanceRecordResult((int) $record->id, 'staff', $data->staffId, $data->status, true); + }); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Services/StudentAttendanceService.php b/app/Domain/SchoolCore/Attendance/Services/StudentAttendanceService.php new file mode 100644 index 0000000..627de31 --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Services/StudentAttendanceService.php @@ -0,0 +1,57 @@ +transactions->run(function () use ($context, $data): AttendanceRecordResult { + $record = $this->records->upsertRecord($context, [ + 'attendance_session_id' => $data->sessionId, + 'attendance_date' => $data->attendanceDate, + 'subject_type' => 'student', + 'subject_id' => $data->studentId, + ], [ + 'status' => $data->status, + 'source' => $data->source, + 'first_seen_at' => $data->recordedAt->format('Y-m-d H:i:s'), + 'last_seen_at' => $data->recordedAt->format('Y-m-d H:i:s'), + 'recorded_by_user_id' => $context->actorUserId, + 'override_reason' => $data->overrideReason, + 'metadata' => json_encode($data->metadata, JSON_THROW_ON_ERROR), + ]); + + $this->audit->log($context, new AttendanceAuditData( + action: 'attendance.student.recorded', + subjectType: 'student', + subjectId: $data->studentId, + attendanceSessionId: $data->sessionId, + attendanceRecordId: (int) $record->id, + after: ['status' => $data->status], + reason: $data->reason ?? $data->overrideReason, + source: $data->source, + idempotencyKey: $data->idempotencyKey, + )); + + return new AttendanceRecordResult((int) $record->id, 'student', $data->studentId, $data->status, true); + }); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Support/AttendanceClock.php b/app/Domain/SchoolCore/Attendance/Support/AttendanceClock.php new file mode 100644 index 0000000..232e76e --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Support/AttendanceClock.php @@ -0,0 +1,17 @@ +timezone)); + } +} diff --git a/app/Domain/SchoolCore/Attendance/Support/AttendanceIdempotencyKey.php b/app/Domain/SchoolCore/Attendance/Support/AttendanceIdempotencyKey.php new file mode 100644 index 0000000..435bd1a --- /dev/null +++ b/app/Domain/SchoolCore/Attendance/Support/AttendanceIdempotencyKey.php @@ -0,0 +1,15 @@ +scannedAt->getTimestamp(), max(1, $windowSeconds)); + + return hash('sha256', implode('|', [ + $context->schoolId, + mb_strtolower(trim($data->badgeValue)), + $data->stationId ?? '', + $data->scanAction, + $data->scanSource, + $bucket, + ])); + } + + public static function payloadHash(ProcessScanData $data): string + { + return hash('sha256', json_encode([ + 'badge_value' => $data->badgeValue, + 'station_id' => $data->stationId, + 'scan_action' => $data->scanAction, + 'scan_source' => $data->scanSource, + 'scanned_at' => $data->scannedAt->format(DATE_ATOM), + 'device_id' => $data->deviceId, + 'client_request_id' => $data->clientRequestId, + 'metadata' => $data->metadata, + ], JSON_THROW_ON_ERROR)); + } +} diff --git a/app/Domain/SchoolCore/Authorization/README.md b/app/Domain/SchoolCore/Authorization/README.md new file mode 100644 index 0000000..8433f6e --- /dev/null +++ b/app/Domain/SchoolCore/Authorization/README.md @@ -0,0 +1,3 @@ +# Authorization + +Neutral SchoolCore module boundary. Keep domain-specific vocabulary in extension modules. diff --git a/app/Domain/SchoolCore/Communication/Channels/EmailChannel.php b/app/Domain/SchoolCore/Communication/Channels/EmailChannel.php new file mode 100644 index 0000000..bd7102d --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Channels/EmailChannel.php @@ -0,0 +1 @@ +self::class]); } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Channels/NotificationChannel.php b/app/Domain/SchoolCore/Communication/Channels/NotificationChannel.php new file mode 100644 index 0000000..2ad490d --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Channels/NotificationChannel.php @@ -0,0 +1 @@ +self::class]); } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Channels/SmsChannel.php b/app/Domain/SchoolCore/Communication/Channels/SmsChannel.php new file mode 100644 index 0000000..01a1c32 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Channels/SmsChannel.php @@ -0,0 +1 @@ +self::class]); } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Channels/WhatsAppChannel.php b/app/Domain/SchoolCore/Communication/Channels/WhatsAppChannel.php new file mode 100644 index 0000000..ccf7157 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Channels/WhatsAppChannel.php @@ -0,0 +1 @@ +self::class]); } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Contracts/ChannelRouterContract.php b/app/Domain/SchoolCore/Communication/Contracts/ChannelRouterContract.php new file mode 100644 index 0000000..3720023 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Contracts/ChannelRouterContract.php @@ -0,0 +1,4 @@ +status==='sent'||$this->status==='queued'; } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/DTO/MessageTemplateData.php b/app/Domain/SchoolCore/Communication/DTO/MessageTemplateData.php new file mode 100644 index 0000000..d7864f8 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/DTO/MessageTemplateData.php @@ -0,0 +1 @@ +email?strtolower(trim($this->email)):null; $phone=$this->phone?preg_replace('/\D+/', '', $this->phone):null; return $this->recipientType.':'.(string)$this->recipientId.':'.($email ?: $phone ?: md5($this->displayName)); } + public function hasChannel(string $channel): bool { return in_array($channel, $this->deliveryChannels, true); } +} diff --git a/app/Domain/SchoolCore/Communication/DTO/RecipientPreviewData.php b/app/Domain/SchoolCore/Communication/DTO/RecipientPreviewData.php new file mode 100644 index 0000000..b35eccb --- /dev/null +++ b/app/Domain/SchoolCore/Communication/DTO/RecipientPreviewData.php @@ -0,0 +1 @@ +excluded; foreach($recipients->recipients as $r){ if($r->schoolId!==$context->schoolId){$excluded[]=['recipient'=>$r->displayName,'reason'=>'wrong_school']; continue;} if(!$r->hasChannel($channel->value)){ $excluded[]=['recipient'=>$r->displayName,'reason'=>'missing_channel']; continue;} $allowed[]=$r; } return new RecipientSet($allowed,$excluded); } public function canOverridePreferences(SchoolContext $context, string $category): bool { return $category==='emergency'; } } diff --git a/app/Domain/SchoolCore/Communication/Policies/DefaultRecipientAuthorizationPolicy.php b/app/Domain/SchoolCore/Communication/Policies/DefaultRecipientAuthorizationPolicy.php new file mode 100644 index 0000000..090eccf --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Policies/DefaultRecipientAuthorizationPolicy.php @@ -0,0 +1 @@ +schoolId === $context->schoolId; } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Repositories/CommunicationLogRepository.php b/app/Domain/SchoolCore/Communication/Repositories/CommunicationLogRepository.php new file mode 100644 index 0000000..3e5e363 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Repositories/CommunicationLogRepository.php @@ -0,0 +1,3 @@ +audienceType === 'class'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian', $context->schoolId * 1000 + 1, 'Class Guardian', 'class-guardian@example.test', '+15555550102', $query->language, [], null, null, $context->schoolId, [$query->channel->value], [], ['resolver'=>self::class], ['class'])]); } } diff --git a/app/Domain/SchoolCore/Communication/Resolvers/GuardianRecipientResolver.php b/app/Domain/SchoolCore/Communication/Resolvers/GuardianRecipientResolver.php new file mode 100644 index 0000000..8477925 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Resolvers/GuardianRecipientResolver.php @@ -0,0 +1,4 @@ +audienceType === 'guardians'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('guardian', $context->schoolId * 1000 + 1, 'Sample Guardian', 'guardian@example.test', '+15555550100', $query->language, [], null, null, $context->schoolId, [$query->channel->value], [], ['resolver'=>self::class], ['guardians'])]); } } diff --git a/app/Domain/SchoolCore/Communication/Resolvers/SchoolWideRecipientResolver.php b/app/Domain/SchoolCore/Communication/Resolvers/SchoolWideRecipientResolver.php new file mode 100644 index 0000000..0dd39cc --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Resolvers/SchoolWideRecipientResolver.php @@ -0,0 +1,4 @@ +audienceType === 'school_wide'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('staff', $context->schoolId * 1000 + 1, 'School Staff', 'school@example.test', '+15555550103', $query->language, [], null, null, $context->schoolId, [$query->channel->value], [], ['resolver'=>self::class], ['school_wide'])]); } } diff --git a/app/Domain/SchoolCore/Communication/Resolvers/StaffRecipientResolver.php b/app/Domain/SchoolCore/Communication/Resolvers/StaffRecipientResolver.php new file mode 100644 index 0000000..ac05218 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Resolvers/StaffRecipientResolver.php @@ -0,0 +1,4 @@ +audienceType === 'staff'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('staff', $context->schoolId * 1000 + 1, 'Sample Staff', 'staff@example.test', '+15555550101', $query->language, [], null, null, $context->schoolId, [$query->channel->value], [], ['resolver'=>self::class], ['staff'])]); } } diff --git a/app/Domain/SchoolCore/Communication/Resolvers/StudentRecipientResolver.php b/app/Domain/SchoolCore/Communication/Resolvers/StudentRecipientResolver.php new file mode 100644 index 0000000..96bf05e --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Resolvers/StudentRecipientResolver.php @@ -0,0 +1,4 @@ +audienceType === 'students'; } public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet { return new RecipientSet([new RecipientData('student', $context->schoolId * 1000 + 1, 'Sample Student', 'student@example.test', '', $query->language, [], null, null, $context->schoolId, [$query->channel->value], [], ['resolver'=>self::class], ['students'])]); } } diff --git a/app/Domain/SchoolCore/Communication/Services/ChannelRouter.php b/app/Domain/SchoolCore/Communication/Services/ChannelRouter.php new file mode 100644 index 0000000..2fa1d90 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/ChannelRouter.php @@ -0,0 +1 @@ +channel->value){ 'email'=>$this->email->send($context,$message), 'sms'=>$this->sms->send($context,$message), 'whatsapp'=>$this->whatsApp->send($context,$message), 'notification'=>$this->notification->send($context,$message), default=>new DeliveryResult('failed',null,'unsupported_channel') }; } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Services/CommunicationAuditLogger.php b/app/Domain/SchoolCore/Communication/Services/CommunicationAuditLogger.php new file mode 100644 index 0000000..15067e2 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/CommunicationAuditLogger.php @@ -0,0 +1 @@ +schoolId.':'.$context->actorUserId.':'.$operation.':'.$key; $hash=hash('sha256',json_encode($payload)); $stored=Cache::get($cacheKey); if($stored){ if(($stored['hash']??null)!==$hash){ throw new DuplicateSendException('Idempotency key reused with different payload.'); } return $stored['result']; } $result=$callback(); Cache::put($cacheKey,['hash'=>$hash,'result'=>$result],now()->addDay()); return $result; } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Services/CommunicationService.php b/app/Domain/SchoolCore/Communication/Services/CommunicationService.php new file mode 100644 index 0000000..31fb232 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/CommunicationService.php @@ -0,0 +1 @@ +idempotency->rememberOrFail($context,$data->idempotencyKey,'communication.send.single',(array)$data,function() use($context,$data){ $delivery=$this->router->route($context,new ChannelMessageData($data->channel,$data->recipient,$data->body,$data->subject)); return new SendResult(null,$delivery); }); } public function sendBulkMessage(SchoolContext $context, BulkSendMessageData $data): BulkSendResult { if($data->previewId===''||$data->confirmationToken===''){ throw new RecipientPreviewRequiredException('Bulk send requires preview_id and confirmation_token.'); } return $this->idempotency->rememberOrFail($context,$data->idempotencyKey,'communication.send.bulk',(array)$data,function() use($context,$data){ $row=DB::table('communication_recipient_previews')->where('school_id',$context->schoolId)->where('id',$data->previewId)->first(); if(!$row || hash('sha256',$data->confirmationToken)!==$row->confirmation_token_hash || now()->greaterThan($row->expires_at)){ throw new RecipientPreviewRequiredException('Preview missing, expired, or token mismatch.'); } $preview=$this->previews->getPreview($context,$data->previewId); $sent=0; $failed=0; $results=[]; foreach($preview->recipients->recipients as $recipient){ $r=is_array($recipient) ? new RecipientData(...array_values($recipient)) : $recipient; $delivery=$this->router->route($context,new ChannelMessageData($data->channel,$r,$data->body,$data->subject)); $delivery->successful() ? $sent++ : $failed++; $results[]=$delivery; } return new BulkSendResult(null,$sent,count($preview->recipients->excluded),$failed,$results); }); } public function scheduleBulkMessage(SchoolContext $context, BulkSendMessageData $data): ScheduledMessageResult { if($data->scheduledAt===null){ throw new RecipientPreviewRequiredException('scheduled_at is required.'); } return new ScheduledMessageResult(null,$data->scheduledAt); } } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Services/DeliveryStatusService.php b/app/Domain/SchoolCore/Communication/Services/DeliveryStatusService.php new file mode 100644 index 0000000..1f075fc --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/DeliveryStatusService.php @@ -0,0 +1 @@ +$receipt->status]; } public function markFailed(SchoolContext $context, int $messageRecipientId, string $reason): void {} } \ No newline at end of file diff --git a/app/Domain/SchoolCore/Communication/Services/MessageTemplateService.php b/app/Domain/SchoolCore/Communication/Services/MessageTemplateService.php new file mode 100644 index 0000000..904799c --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/MessageTemplateService.php @@ -0,0 +1,4 @@ +$data->templateKey]; } public function render(SchoolContext $context, string $templateKey, array $variables=[], ?string $language=null): string { return ''; } public function archive(SchoolContext $context, string $templateKey, string $reason): void {} } diff --git a/app/Domain/SchoolCore/Communication/Services/RecipientDedupeService.php b/app/Domain/SchoolCore/Communication/Services/RecipientDedupeService.php new file mode 100644 index 0000000..6582b52 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/RecipientDedupeService.php @@ -0,0 +1,4 @@ +excluded; foreach($recipients->recipients as $recipient){ $key=$recipient->fingerprint(); if(isset($seen[$key])){ $excluded[]=['recipient'=>$recipient->displayName,'reason'=>'duplicate']; continue; } $seen[$key]=true; $kept[]=$recipient; } return new RecipientSet($kept,$excluded); } } diff --git a/app/Domain/SchoolCore/Communication/Services/RecipientPreviewService.php b/app/Domain/SchoolCore/Communication/Services/RecipientPreviewService.php new file mode 100644 index 0000000..6ec8aa7 --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Services/RecipientPreviewService.php @@ -0,0 +1,4 @@ +resolvers as $resolver){ if($resolver->supports($query)){ $resolved=$resolved->merge($resolver->resolve($context,$query)); } } if($resolved->count()===0){ throw new RecipientResolutionException('No resolver produced recipients.'); } $deduped=$this->dedupe->dedupe($resolved); $allowed=$this->preferences->filterAllowedRecipients($context,$deduped,$query->channel,$query->messageCategory); $previewId=(string)Str::uuid(); $token=bin2hex(random_bytes(16)); $expiresAt=now()->addMinutes(30)->toDateTimeString(); DB::table('communication_recipient_previews')->insert(['id'=>$previewId,'school_id'=>$context->schoolId,'actor_user_id'=>$context->actorUserId,'audience_type'=>$query->audienceType,'audience_filters_json'=>json_encode($query->audienceFilters),'channel'=>$query->channel->value,'message_category'=>$query->messageCategory,'recipient_count'=>$resolved->count(),'deduped_count'=>$deduped->count(),'blocked_count'=>count($allowed->excluded),'missing_contact_count'=>0,'snapshot_json'=>json_encode(['recipients'=>$allowed->recipients,'excluded'=>$allowed->excluded]),'confirmation_token_hash'=>hash('sha256',$token),'expires_at'=>$expiresAt,'created_at'=>now(),'updated_at'=>now()]); return new RecipientPreviewResult($previewId,$allowed,$resolved->count(),$deduped->count(),count($allowed->excluded),0,0,$allowed->sample(),[],$token,$expiresAt); } public function getPreview(SchoolContext $context, string $previewId): RecipientPreviewResult { $row=DB::table('communication_recipient_previews')->where('school_id',$context->schoolId)->where('id',$previewId)->first(); if(!$row){ throw new RecipientResolutionException('Preview not found.'); } $snap=json_decode((string)$row->snapshot_json,true)?:[]; $set=new RecipientSet($snap['recipients']??[],$snap['excluded']??[]); return new RecipientPreviewResult((string)$row->id,$set,(int)$row->recipient_count,(int)$row->deduped_count,(int)$row->blocked_count,(int)$row->missing_contact_count,0,$set->sample(),[],'',(string)$row->expires_at); } } diff --git a/app/Domain/SchoolCore/Communication/Support/CommunicationTransactionRunner.php b/app/Domain/SchoolCore/Communication/Support/CommunicationTransactionRunner.php new file mode 100644 index 0000000..82536fe --- /dev/null +++ b/app/Domain/SchoolCore/Communication/Support/CommunicationTransactionRunner.php @@ -0,0 +1,3 @@ +