inti project

This commit is contained in:
root
2026-05-29 04:33:03 -04:00
commit cdeab1796f
699 changed files with 20516 additions and 0 deletions
+18
View File
@@ -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
+65
View File
@@ -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}"
+11
View File
@@ -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
+27
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
ignore-scripts=true
audit=true
+289
View File
@@ -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.
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Support\Architecture\ArchitectureScanner;
use Illuminate\Console\Command;
final class ArchitectureScanCommand extends Command
{
protected $signature = 'app:architecture-scan {--format=console : console or json} {--fail-on=error : error, warning, or none}';
protected $description = 'Scan the codebase for modular architecture boundary violations.';
public function handle(ArchitectureScanner $scanner): int
{
$violations = $scanner->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;
}
}
@@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Support\Architecture\DependencyMapGenerator;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
final class DependencyMapCommand extends Command
{
protected $signature = 'app:dependency-map {--markdown : Also write docs/architecture/dependency-map.md}';
protected $description = 'Generate a module dependency map for governance review.';
public function handle(DependencyMapGenerator $generator): int
{
$map = $generator->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;
}
}
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
final class DocumentationCoverageCommand extends Command
{
protected $signature = 'app:docs-coverage {--inventory=storage/app/generated/api-route-inventory.json} {--openapi=docs/openapi.phase8.yaml}';
protected $description = 'Check that canonical routes and deprecated routes are represented in documentation artifacts.';
public function handle(): int
{
$inventoryPath = base_path((string) $this->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;
}
}
@@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Storage;
final class GenerateApiRouteInventoryCommand extends Command
{
protected $signature = 'api:route-inventory {--markdown : Also write docs/api-route-inventory.md}';
protected $description = 'Generate API route inventory for controller cleanup and OpenAPI coverage.';
public function handle(): int
{
$classification = config('api_controller_classification.controllers', []);
$rows = collect(RouteFacade::getRoutes())
->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;
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
final class LegacyUnsafeRouteAuditCommand extends Command
{
protected $signature = 'app:legacy-unsafe-route-audit';
protected $description = 'Audit known unsafe legacy routes that must be disabled or removed before final rollout.';
public function handle(): int
{
$items = config('release_readiness.unsafe_legacy_routes', []);
$report = [
'generated_at' => 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;
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Support\Release\FeatureFlagRegistry;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
final class ReleaseFeatureFlagsCommand extends Command
{
protected $signature = 'app:release-feature-flags {--format=json}';
protected $description = 'Export modular rollout feature flag map.';
public function handle(FeatureFlagRegistry $registry): int
{
$data = ['flags' => $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;
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use App\Support\Release\MigrationValidationRunner;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
final class ReleaseMigrationValidateCommand extends Command
{
protected $signature = 'app:release-migration-validate {--module=all}';
protected $description = 'Run modular migration validation checks.';
public function handle(MigrationValidationRunner $runner): int
{
$results = array_map(fn ($result): array => $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;
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Support\Release\ReleaseReadinessGate;
use Illuminate\Console\Command;
final class ReleaseReadinessGateCommand extends Command
{
protected $signature = 'app:release-readiness-gate {--warning-mode}';
protected $description = 'Evaluate modular release readiness artifacts and gates.';
public function handle(ReleaseReadinessGate $gate): int
{
$result = $gate->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;
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use App\Support\Release\ShadowComparisonRunner;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
final class ReleaseShadowCompareCommand extends Command
{
protected $signature = 'app:release-shadow-compare {--module=all}';
protected $description = 'Run shadow comparisons between legacy and modular outputs.';
public function handle(ShadowComparisonRunner $runner): int
{
$results = array_map(fn ($result): array => $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;
}
}
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
final class RouteInventoryCheckCommand extends Command
{
protected $signature = 'app:route-inventory-check {--path=storage/app/generated/api-route-inventory.json}';
protected $description = 'Check generated route inventory for governance metadata gaps.';
public function handle(): int
{
$path = base_path((string) $this->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;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Contracts\AttendancePolicyContract;
use DateTimeInterface;
final class IslamicSundaySchoolAttendancePolicy implements AttendancePolicyContract
{
public function isSchoolDay(SchoolContext $context, DateTimeInterface $date): bool
{
return (int) $date->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;
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Policies;
use App\Domain\SchoolCore\Attendance\Policies\DefaultAttendancePolicy;
use App\Domain\SchoolCore\Context\SchoolContext;
final class IslamicSundaySchoolAttendancePolicy extends DefaultAttendancePolicy
{
public function duplicateScanWindowSeconds(SchoolContext $context): int
{
return 180;
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Policies;
use App\Domain\SchoolCore\Attendance\Contracts\AttendanceStatusPolicyContract;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceStatusResult;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeInterface;
final class IslamicSundaySchoolAttendanceStatusPolicy implements AttendanceStatusPolicyContract
{
public function statusForScan(SchoolContext $context, AttendanceSessionView $session, DateTimeInterface $timestamp): AttendanceStatusResult
{
$lateThreshold = $session->startsAt->modify('+15 minutes');
$status = $timestamp > $lateThreshold ? 'late' : 'present';
return new AttendanceStatusResult($status, $status === 'late', $status === 'late' ? 'Configured extension late window.' : null);
}
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Policies;
final class IslamicSundaySchoolSessionPolicy
{
public function supportsProgramSessionType(string $type): bool
{
return in_array($type, ['program', 'group', 'class', 'event'], true);
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Providers;
use App\Domain\IslamicSundaySchool\Attendance\Policies\IslamicSundaySchoolAttendancePolicy;
use App\Domain\IslamicSundaySchool\Attendance\Policies\IslamicSundaySchoolAttendanceStatusPolicy;
use App\Domain\IslamicSundaySchool\Attendance\Services\IslamicSundaySchoolSessionResolver;
use App\Domain\SchoolCore\Attendance\Contracts\AttendancePolicyContract;
use App\Domain\SchoolCore\Attendance\Contracts\AttendanceSessionResolverContract;
use App\Domain\SchoolCore\Attendance\Contracts\AttendanceStatusPolicyContract;
use Illuminate\Support\ServiceProvider;
final class IslamicSundaySchoolAttendanceServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(AttendancePolicyContract::class, IslamicSundaySchoolAttendancePolicy::class);
$this->app->bind(AttendanceStatusPolicyContract::class, IslamicSundaySchoolAttendanceStatusPolicy::class);
$this->app->bind(AttendanceSessionResolverContract::class, IslamicSundaySchoolSessionResolver::class);
}
}
@@ -0,0 +1,3 @@
# Attendance
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Reports;
final class IslamicSundaySchoolAttendanceReportBuilder
{
public function labels(): array
{
return [
'present' => 'Present',
'late' => 'Late',
'absent' => 'Absent',
];
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Services;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use DateTimeInterface;
final class IslamicSundaySchoolLateArrivalEvaluator
{
public function isLate(AttendanceSessionView $session, DateTimeInterface $timestamp): bool
{
return $timestamp > $session->startsAt->modify('+15 minutes');
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Attendance\Services;
use App\Domain\SchoolCore\Attendance\Contracts\AttendanceSessionResolverContract;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSubjectView;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use App\Domain\SchoolCore\Attendance\Services\AttendanceSessionResolver;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeInterface;
final class IslamicSundaySchoolSessionResolver implements AttendanceSessionResolverContract
{
public function __construct(private AttendanceSessionResolver $fallback) {}
public function resolveForScan(SchoolContext $context, AttendanceSubjectView $subject, DateTimeInterface $timestamp): AttendanceSessionView
{
return $this->fallback->resolveForScan($context, $subject, $timestamp);
}
}
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Policies; use App\Domain\SchoolCore\Communication\Policies\DefaultCommunicationPreferencePolicy; final class IslamicSundaySchoolCommunicationPreferencePolicy extends DefaultCommunicationPreferencePolicy {}
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Policies; final class IslamicSundaySchoolRecipientAuthorizationPolicy {}
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Providers; use Illuminate\Support\ServiceProvider; use App\Domain\IslamicSundaySchool\Communication\Resolvers\{SundayClassRecipientResolver,VolunteerTeamRecipientResolver,MasjidFamilyRecipientResolver,VolunteerRecipientResolver,IslamicSundaySchoolProgramRecipientResolver}; final class IslamicSundaySchoolCommunicationServiceProvider extends ServiceProvider { public function register(): void { $this->app->tag([SundayClassRecipientResolver::class,VolunteerTeamRecipientResolver::class,MasjidFamilyRecipientResolver::class,VolunteerRecipientResolver::class,IslamicSundaySchoolProgramRecipientResolver::class], 'communication.recipient_resolvers'); } }
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Resolvers; use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Communication\Contracts\RecipientResolverContract; use App\Domain\SchoolCore\Communication\DTO\{RecipientData,RecipientQueryData}; use App\Domain\SchoolCore\Communication\Support\RecipientSet; final class IslamicSundaySchoolProgramRecipientResolver implements RecipientResolverContract { public function supports(RecipientQueryData $query): bool { return $query->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'])]); } }
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Resolvers; use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Communication\Contracts\RecipientResolverContract; use App\Domain\SchoolCore\Communication\DTO\{RecipientData,RecipientQueryData}; use App\Domain\SchoolCore\Communication\Support\RecipientSet; final class MasjidFamilyRecipientResolver implements RecipientResolverContract { public function supports(RecipientQueryData $query): bool { return $query->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'])]); } }
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Resolvers; use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Communication\Contracts\RecipientResolverContract; use App\Domain\SchoolCore\Communication\DTO\{RecipientData,RecipientQueryData}; use App\Domain\SchoolCore\Communication\Support\RecipientSet; final class SundayClassRecipientResolver implements RecipientResolverContract { public function supports(RecipientQueryData $query): bool { return $query->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'])]); } }
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Resolvers; use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Communication\Contracts\RecipientResolverContract; use App\Domain\SchoolCore\Communication\DTO\{RecipientData,RecipientQueryData}; use App\Domain\SchoolCore\Communication\Support\RecipientSet; final class VolunteerRecipientResolver implements RecipientResolverContract { public function supports(RecipientQueryData $query): bool { return $query->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'])]); } }
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Resolvers; use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Communication\Contracts\RecipientResolverContract; use App\Domain\SchoolCore\Communication\DTO\{RecipientData,RecipientQueryData}; use App\Domain\SchoolCore\Communication\Support\RecipientSet; final class VolunteerTeamRecipientResolver implements RecipientResolverContract { public function supports(RecipientQueryData $query): bool { return $query->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'])]); } }
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Templates; final class IslamicSundaySchoolTemplateCatalog { public function templates(): array { return ['program_class_reminder'=>['category'=>'event'], 'volunteer_reminder'=>['category'=>'event'], 'attendance_follow_up'=>['category'=>'attendance']]; } }
@@ -0,0 +1,3 @@
# Contracts
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,3 @@
# Curriculum
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,3 @@
# Families
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Finance\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Finance\Contracts\PaymentAllocationPolicyContract;
use App\Domain\SchoolCore\Finance\Support\Money;
final class IslamicSundaySchoolPaymentAllocationPolicy implements PaymentAllocationPolicyContract
{
public function allocate(SchoolContext $context, array $invoiceView, Money $paymentAmount): array
{
return [['invoice_id'=>$invoiceView['id'] ?? null,'amount_minor'=>$paymentAmount->minorAmount(),'currency'=>$paymentAmount->currency(),'strategy'=>'extension_policy_family_program_order']];
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Finance\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Finance\Contracts\TuitionPolicyContract;
final class IslamicSundaySchoolTuitionPolicy implements TuitionPolicyContract
{
public function calculateExpectedCharges(SchoolContext $context, array $studentFinanceProfile): array
{
return ['profile'=>$context->domainProfile,'charges'=>[],'labels_are_extension_owned'=>true];
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Finance\Providers;
use App\Domain\IslamicSundaySchool\Finance\Policies\IslamicSundaySchoolPaymentAllocationPolicy;
use App\Domain\IslamicSundaySchool\Finance\Policies\IslamicSundaySchoolTuitionPolicy;
use App\Domain\SchoolCore\Finance\Contracts\PaymentAllocationPolicyContract;
use App\Domain\SchoolCore\Finance\Contracts\TuitionPolicyContract;
use Illuminate\Support\ServiceProvider;
final class IslamicSundaySchoolFinanceServiceProvider extends ServiceProvider
{
public function register(): void
{
if (! (bool) config('school_context.extensions.islamic_sunday_school.enabled', false)) { return; }
$this->app->bind(TuitionPolicyContract::class, IslamicSundaySchoolTuitionPolicy::class);
$this->app->bind(PaymentAllocationPolicyContract::class, IslamicSundaySchoolPaymentAllocationPolicy::class);
}
}
@@ -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.
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Finance\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Finance\Repositories\FinanceReadRepository;
final class IslamicSundaySchoolFinanceReportBuilder
{
public function __construct(private readonly FinanceReadRepository $finance) {}
public function outstandingBalances(SchoolContext $context): array
{
return ['domain_profile'=>$context->domainProfile,'rows'=>$this->finance->outstandingInvoices($context)];
}
}
@@ -0,0 +1,3 @@
# Ministry
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Contracts\PaymentAllocationPolicyContract;
final class IslamicSundaySchoolPaymentAllocationPolicy implements PaymentAllocationPolicyContract
{
public function allocate(SchoolContext $context, int $invoiceId, float $amount): array
{
return [['invoice_id' => $invoiceId, 'amount' => $amount, 'allocation_rule' => 'extension_default']];
}
}
@@ -0,0 +1,3 @@
# Policies
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Providers;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Contracts\AcademicCalendarProviderContract;
use DateTimeInterface;
final class IslamicSundaySchoolCalendarProvider implements AcademicCalendarProviderContract
{
public function currentAcademicYear(SchoolContext $context): ?int
{
return is_numeric($context->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 [];
}
}
@@ -0,0 +1,3 @@
# Providers
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,3 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ExportTemplates;
final class FamilySummaryExportTemplate {}
@@ -0,0 +1,3 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ExportTemplates;
final class QuranProgressExportTemplate {}
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\IslamicSundaySchool\Reporting\Reports\{QuranProgressReport,ArabicLevelReport,IslamicStudiesProgressReport,HalaqaAttendanceReport,SundayProgramAttendanceReport,MasjidFamilySummaryReport,VolunteerTeamReport,IslamicSundaySchoolDashboardReport};
final class IslamicSundaySchoolReportingServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->tag([
QuranProgressReport::class,
ArabicLevelReport::class,
IslamicStudiesProgressReport::class,
HalaqaAttendanceReport::class,
SundayProgramAttendanceReport::class,
MasjidFamilySummaryReport::class,
VolunteerTeamReport::class,
IslamicSundaySchoolDashboardReport::class,
], 'schoolcore.reports');
}
}
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ReadModels;
use App\Domain\SchoolCore\Context\SchoolContext;
final class ArabicProgressReadModel { public function rows(SchoolContext $context, array $filters = []): array { return []; } }
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ReadModels;
use App\Domain\SchoolCore\Context\SchoolContext;
final class HalaqaReportReadModel { public function rows(SchoolContext $context, array $filters = []): array { return []; } }
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ReadModels;
use App\Domain\SchoolCore\Context\SchoolContext;
final class IslamicStudiesProgressReadModel { public function rows(SchoolContext $context, array $filters = []): array { return []; } }
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ReadModels;
use App\Domain\SchoolCore\Context\SchoolContext;
final class MasjidFamilyReportReadModel { public function rows(SchoolContext $context, array $filters = []): array { return []; } }
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\ReadModels;
use App\Domain\SchoolCore\Context\SchoolContext;
final class QuranProgressReadModel { public function rows(SchoolContext $context, array $filters = []): array { return []; } }
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class ArabicLevelReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.arabic_level'; }
public function name(): string { return 'Arabic Level'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Reporting\Enums\ReportCategory;
use App\Domain\SchoolCore\Reporting\Reports\BaseReportDefinition;
abstract class BaseIslamicSundaySchoolReport extends BaseReportDefinition
{
public function category(): ReportCategory { return ReportCategory::EXTENSION; }
public function availableForDomainProfile(?string $domainProfile): bool { return $domainProfile === 'islamic_sunday_school'; }
public function requiredPermissions(): array { return ['report.view', 'report.islamic.view']; }
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class HalaqaAttendanceReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.halaqa_attendance'; }
public function name(): string { return 'Halaqa Attendance'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class IslamicStudiesProgressReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.studies_progress'; }
public function name(): string { return 'Islamic Studies Progress'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class IslamicSundaySchoolDashboardReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.dashboard'; }
public function name(): string { return 'Islamic Sunday School Dashboard'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class MasjidFamilySummaryReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.masjid_family_summary'; }
public function name(): string { return 'Masjid Family Summary'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class QuranProgressReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.quran_progress'; }
public function name(): string { return "Qur'an Progress"; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class SundayProgramAttendanceReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.sunday_program_attendance'; }
public function name(): string { return 'Sunday Program Attendance'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Reporting\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Reporting\DTO\ReportResultData;
final class VolunteerTeamReport extends BaseIslamicSundaySchoolReport
{
public function key(): string { return 'islamic.volunteer_team'; }
public function name(): string { return 'Volunteer Team'; }
public function columns(): array { return [['key' => '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'));
}
}
@@ -0,0 +1,3 @@
# Reports
Extension module. Bind policies/providers here without leaking implementation details into SchoolCore.
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\DTO;
final readonly class IslamicSundaySchoolStudentProfileData { public function __construct(public int $studentId, public ?int $familyProfileId=null, public ?int $quranLevelId=null, public ?int $arabicLevelId=null, public ?int $islamicStudiesTrackId=null, public ?int $halaqaGroupId=null, public ?string $memorizationProgressSummary=null, public ?string $recitationPlacement=null, public bool $volunteerFamilyParticipation=false, public ?string $sensitiveAdminNotes=null, public array $metadata=[]) {} }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Policies;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\DTO\EnrollmentData;
final class IslamicSundaySchoolEnrollmentPolicy { public function canEnroll(SchoolContext $context, EnrollmentData $data): bool { return $context->isIslamicSundaySchool() && $data->studentId>0; } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Policies;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\PromotionPolicyContract; use App\Domain\SchoolCore\Students\DTO\EnrollmentView; use App\Domain\SchoolCore\Students\DTO\PromotionEvaluationResult; use App\Domain\SchoolCore\Students\DTO\StudentView;
final class IslamicSundaySchoolPromotionPolicy implements PromotionPolicyContract { public function evaluate(SchoolContext $context, StudentView $student, EnrollmentView $enrollment): PromotionEvaluationResult { return new PromotionEvaluationResult($context->isIslamicSundaySchool() && $student->status==='active',$student->status==='active'?'promoted':'manual_review'); } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Policies;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\StudentIdentifierGeneratorContract; use App\Domain\SchoolCore\Students\DTO\StudentIdentifier; use App\Domain\SchoolCore\Students\DTO\StudentView;
final class IslamicSundaySchoolStudentIdentifierGenerator implements StudentIdentifierGeneratorContract { public function generateForPersistedStudent(SchoolContext $context, StudentView $student): StudentIdentifier { return new StudentIdentifier(sprintf('SS-%s-%06d',date('Y'),$student->id)); } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Providers;
use App\Domain\IslamicSundaySchool\Students\Policies\IslamicSundaySchoolPromotionPolicy; use App\Domain\IslamicSundaySchool\Students\Policies\IslamicSundaySchoolStudentIdentifierGenerator; use App\Domain\SchoolCore\Students\Contracts\PromotionPolicyContract; use App\Domain\SchoolCore\Students\Contracts\StudentIdentifierGeneratorContract; use Illuminate\Support\ServiceProvider;
final class IslamicSundaySchoolStudentServiceProvider extends ServiceProvider { public function register(): void { if((bool)config('school_context.extensions.islamic_sunday_school.students',false)){ $this->app->bind(StudentIdentifierGeneratorContract::class,IslamicSundaySchoolStudentIdentifierGenerator::class); $this->app->bind(PromotionPolicyContract::class,IslamicSundaySchoolPromotionPolicy::class); } } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Reports;
use App\Domain\SchoolCore\Context\SchoolContext;
final class IslamicSundaySchoolStudentReportBuilder { public function buildSummary(SchoolContext $context,array $filters=[]): array { return ['domain_profile'=>$context->domainProfile,'filters'=>$filters,'labels'=>['quran_level','arabic_level','islamic_studies_track']]; } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext;
final class IslamicSundaySchoolFamilyProfileService { public function summarizeForStudent(SchoolContext $context,int $studentId): array { return ['school_id'=>$context->schoolId,'student_id'=>$studentId,'extension'=>'islamic_sunday_school']; } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\IslamicSundaySchool\Students\Services;
use App\Domain\IslamicSundaySchool\Students\DTO\IslamicSundaySchoolStudentProfileData; use App\Domain\SchoolCore\Context\SchoolContext; use Illuminate\Support\Facades\DB;
final class IslamicSundaySchoolStudentProfileService { public function save(SchoolContext $context, IslamicSundaySchoolStudentProfileData $data): array { DB::table('islamic_sunday_school_student_profiles')->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']; } }
+24
View File
@@ -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.
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Academics\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Contracts\GradeScalePolicyContract;
final class DefaultGradeScalePolicy implements GradeScalePolicyContract
{
public function gradeForScore(SchoolContext $context, float $score): string
{
return match (true) { $score >= 90 => 'A', $score >= 80 => 'B', $score >= 70 => 'C', $score >= 60 => 'D', default => 'F' };
}
public function passingScore(SchoolContext $context): float { return 60.0; }
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Academics\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Contracts\PromotionPolicyContract;
final class DefaultPromotionPolicy implements PromotionPolicyContract
{
public function canPromote(SchoolContext $context, int $studentId): bool { return false; }
public function nextPlacement(SchoolContext $context, int $studentId): array { return []; }
}
@@ -0,0 +1,3 @@
# Academics
Neutral SchoolCore module boundary. Keep domain-specific vocabulary in extension modules.
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceAuditData;
interface AttendanceAuditLoggerContract
{
public function log(SchoolContext $context, AttendanceAuditData $data): void;
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceIdempotencyData;
use App\Domain\SchoolCore\Attendance\DTO\ProcessScanData;
use App\Domain\SchoolCore\Attendance\DTO\ScanResult;
interface AttendanceIdempotencyServiceContract
{
public function findExisting(SchoolContext $context, AttendanceIdempotencyData $data): ?ScanResult;
public function storeResult(SchoolContext $context, AttendanceIdempotencyData $data, ProcessScanData $scan, ScanResult $result): void;
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSubjectView;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
interface AttendancePolicyContract
{
public function canRecordAttendance(SchoolContext $context, AttendanceSubjectView $subject, AttendanceSessionView $session): bool;
public function duplicateScanWindowSeconds(SchoolContext $context): int;
public function allowCheckout(SchoolContext $context): bool;
public function allowManualOverride(SchoolContext $context): bool;
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSummaryQuery;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSummaryResult;
interface AttendanceServiceContract
{
public function getSessionAttendance(SchoolContext $context, int $sessionId): AttendanceSessionView;
public function summarizeAttendance(SchoolContext $context, AttendanceSummaryQuery $query): AttendanceSummaryResult;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSubjectView;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use DateTimeInterface;
interface AttendanceSessionResolverContract
{
public function resolveForScan(SchoolContext $context, AttendanceSubjectView $subject, DateTimeInterface $timestamp): AttendanceSessionView;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceStatusResult;
use DateTimeInterface;
interface AttendanceStatusPolicyContract
{
public function statusForScan(SchoolContext $context, AttendanceSessionView $session, DateTimeInterface $timestamp): AttendanceStatusResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\BadgeLookupData;
use App\Domain\SchoolCore\Attendance\DTO\BadgeLookupResult;
interface BadgeLookupServiceContract
{
public function lookup(SchoolContext $context, BadgeLookupData $data): BadgeLookupResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\ProcessScanData;
use App\Domain\SchoolCore\Attendance\DTO\ScanResult;
interface ScannerServiceContract
{
public function processScan(SchoolContext $context, ProcessScanData $data): ScanResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\RecordStaffAttendanceData;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceRecordResult;
interface StaffAttendanceServiceContract
{
public function recordStaffAttendance(SchoolContext $context, RecordStaffAttendanceData $data): AttendanceRecordResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\DTO\RecordStudentAttendanceData;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceRecordResult;
interface StudentAttendanceServiceContract
{
public function recordStudentAttendance(SchoolContext $context, RecordStudentAttendanceData $data): AttendanceRecordResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceActorData
{
public function __construct(
public int $actorUserId,
public array $roleIds = [],
public string $actorType = 'user',
) {}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceAuditData
{
public function __construct(
public string $action,
public string $subjectType,
public int $subjectId,
public ?int $attendanceSessionId = null,
public ?int $attendanceRecordId = null,
public ?array $before = null,
public ?array $after = null,
public ?string $reason = null,
public string $source = 'system',
public ?string $stationId = null,
public ?string $requestId = null,
public ?string $idempotencyKey = null,
public array $metadata = [],
) {}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceIdempotencyData
{
public function __construct(
public string $operation,
public string $key,
public string $payloadHash,
public ?string $fingerprint = null,
) {}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceRecordResult
{
public function __construct(
public int $recordId,
public string $subjectType,
public int $subjectId,
public string $status,
public bool $created,
public array $payload = [],
) {}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
use DateTimeImmutable;
final readonly class AttendanceSessionData
{
public function __construct(
public string $name,
public string $sessionType,
public DateTimeImmutable $startsAt,
public DateTimeImmutable $endsAt,
public string $timezone,
public array $metadata = [],
) {}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
use DateTimeImmutable;
final readonly class AttendanceSessionView
{
public function __construct(
public int $id,
public int $schoolId,
public string $name,
public string $sessionType,
public DateTimeImmutable $startsAt,
public DateTimeImmutable $endsAt,
public string $timezone,
public string $status = 'open',
public array $metadata = [],
) {}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceStatusData
{
public function __construct(
public string $status,
public string $source,
public ?string $reason = null,
public array $metadata = [],
) {}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceStatusResult
{
public function __construct(
public string $status,
public bool $late = false,
public ?string $reason = null,
public array $metadata = [],
) {}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceSubjectView
{
public function __construct(
public string $subjectType,
public int $subjectId,
public int $schoolId,
public string $displayName,
public bool $active = true,
public array $metadata = [],
) {}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceSummaryQuery
{
public function __construct(
public ?string $fromDate = null,
public ?string $toDate = null,
public ?int $sessionId = null,
public ?string $subjectType = null,
public array $filters = [],
) {}
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class AttendanceSummaryResult
{
public function __construct(
public array $counts,
public array $rows = [],
) {}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class BadgeLookupData
{
public function __construct(
public string $badgeValue,
public ?string $badgeType = null,
public ?string $subjectTypeHint = null,
public ?string $stationId = null,
) {}
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Attendance\DTO;
final readonly class BadgeLookupResult
{
public function __construct(
public AttendanceSubjectView $subject,
public ?int $badgeId = null,
public array $metadata = [],
) {}
}

Some files were not shown because too many files have changed in this diff Show More