inti project
This commit is contained in:
@@ -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
|
||||||
@@ -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}"
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -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.
|
||||||
+17
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -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 {}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<?php declare(strict_types=1); namespace App\Domain\IslamicSundaySchool\Communication\Policies; final class IslamicSundaySchoolRecipientAuthorizationPolicy {}
|
||||||
+1
@@ -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'); } }
|
||||||
+1
@@ -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'])]); } }
|
||||||
+1
@@ -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'])]); } }
|
||||||
+1
@@ -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'])]); } }
|
||||||
+1
@@ -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'])]); } }
|
||||||
+1
@@ -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.
|
||||||
+17
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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.
|
||||||
+17
@@ -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.
|
||||||
+16
@@ -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.
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace App\Domain\IslamicSundaySchool\Reporting\ExportTemplates;
|
||||||
|
final class FamilySummaryExportTemplate {}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace App\Domain\IslamicSundaySchool\Reporting\ExportTemplates;
|
||||||
|
final class QuranProgressExportTemplate {}
|
||||||
+22
@@ -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 []; } }
|
||||||
+4
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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=[]) {} }
|
||||||
+5
@@ -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; } }
|
||||||
+5
@@ -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'); } }
|
||||||
+5
@@ -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)); } }
|
||||||
+5
@@ -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); } } }
|
||||||
+5
@@ -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']]; } }
|
||||||
+5
@@ -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']; } }
|
||||||
+5
@@ -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']; } }
|
||||||
@@ -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
Reference in New Issue
Block a user