update api and add more features
This commit is contained in:
Vendored
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
# Islamic Sunday School Modular Plan Update Index
|
||||
|
||||
This bundle updates the global modular plan and Phases 1-6 so the extension domain is explicitly **Islamic Sunday School**.
|
||||
|
||||
Core rule: `SchoolCore` remains neutral. Islamic-specific behavior belongs in `App\Domain\IslamicSundaySchool` through policies, resolvers, services, templates, and extension profile tables.
|
||||
|
||||
## Updated Files
|
||||
|
||||
- `app_project_plan_global_modular_islamic.md`
|
||||
- `phase_1_school_context_execution_plan_islamic.md`
|
||||
- `phase_2_core_contracts_execution_plan_islamic.md`
|
||||
- `phase_3_modular_finance_execution_plan_islamic.md`
|
||||
- `phase_4_modular_attendance_execution_plan_islamic.md`
|
||||
- `phase_5_modular_student_lifecycle_execution_plan_islamic.md`
|
||||
- `phase_6_modular_communication_execution_plan_islamic.md`
|
||||
|
||||
## Key Renames
|
||||
|
||||
```text
|
||||
App\Domain\SundaySchool -> App\Domain\IslamicSundaySchool
|
||||
sunday_school -> islamic_sunday_school
|
||||
Sunday School profile -> Islamic Sunday School profile
|
||||
church/ministry/pastoral terms -> masjid/community, volunteer/program, imam/admin or religious-sensitive terms
|
||||
Bible curriculum -> Qur'an/Arabic/Islamic studies curriculum
|
||||
```
|
||||
|
||||
## Non-Negotiable Boundary
|
||||
|
||||
`SchoolCore` must not import `IslamicSundaySchool`.
|
||||
|
||||
Allowed:
|
||||
|
||||
```text
|
||||
IslamicSundaySchool -> SchoolCore contracts
|
||||
SchoolCore -> Shared neutral infrastructure
|
||||
Controllers -> contracts
|
||||
```
|
||||
|
||||
Forbidden:
|
||||
|
||||
```text
|
||||
SchoolCore -> IslamicSundaySchool
|
||||
SchoolCore contracts containing Islamic-specific vocabulary
|
||||
Controllers hardcoding Islamic Sunday School rules
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
# Phase 9 module ownership. Replace usernames/team slugs with real owners before enforcing.
|
||||
/app/Domain/SchoolCore/Context/ @platform-owner
|
||||
/app/Domain/SchoolCore/Finance/ @finance-owner @platform-owner
|
||||
/app/Domain/SchoolCore/Attendance/ @attendance-owner @platform-owner
|
||||
/app/Domain/SchoolCore/Students/ @students-owner @platform-owner
|
||||
/app/Domain/SchoolCore/Communication/ @communication-owner @platform-owner
|
||||
/app/Domain/SchoolCore/Reporting/ @reporting-owner @data-security-owner
|
||||
/app/Domain/IslamicSundaySchool/ @islamic-sunday-school-owner @platform-owner
|
||||
/app/Http/Controllers/ @api-owner
|
||||
/routes/ @api-owner @platform-owner
|
||||
/docs/ @platform-owner
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
## Modular Architecture Checklist
|
||||
|
||||
- [ ] SchoolContext is used where required.
|
||||
- [ ] No SchoolCore dependency on IslamicSundaySchool.
|
||||
- [ ] Controllers remain thin and delegate to contracts.
|
||||
- [ ] FormRequest validates mutable endpoint input.
|
||||
- [ ] Resource or ApiResponse controls output.
|
||||
- [ ] Authorization is tested.
|
||||
- [ ] Wrong-school access is tested for scoped data.
|
||||
- [ ] Sensitive fields are protected or masked.
|
||||
- [ ] New route appears in route inventory.
|
||||
- [ ] New report/export is audited.
|
||||
- [ ] Bulk communication requires preview.
|
||||
- [ ] Finance changes avoid float math and direct controller mutation.
|
||||
- [ ] Module owner reviewed.
|
||||
|
||||
## Risk Notes
|
||||
|
||||
List any boundary exceptions, deprecated paths, new routes, new reports/exports, or sensitive data exposure. If this section is empty for a high-risk change, assume the PR is lying by omission.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
name: Architecture Governance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
architecture:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
- name: Validate composer
|
||||
run: composer validate --no-check-publish || true
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist || true
|
||||
- name: Run architecture tests
|
||||
run: php artisan test tests/Architecture || true
|
||||
- name: Run architecture scan in warning mode
|
||||
run: php artisan app:architecture-scan --fail-on=none || true
|
||||
- name: Generate route inventory
|
||||
run: php artisan api:route-inventory --markdown || true
|
||||
- name: Check route inventory
|
||||
run: php artisan app:route-inventory-check || true
|
||||
- name: Generate dependency map
|
||||
run: php artisan app:dependency-map --markdown || true
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
name: Release Gate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
- run: composer install --no-interaction --prefer-dist || true
|
||||
- run: php artisan test tests/Architecture
|
||||
- run: php artisan app:architecture-scan --fail-on=error
|
||||
- run: php artisan api:route-inventory --markdown
|
||||
- run: php artisan app:route-inventory-check
|
||||
- run: php artisan app:docs-coverage
|
||||
- run: php artisan app:dependency-map --markdown
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
name: Release Readiness
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'config/**'
|
||||
- 'routes/**'
|
||||
- 'docs/release/**'
|
||||
- '.github/workflows/release-readiness.yml'
|
||||
|
||||
jobs:
|
||||
release-readiness:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: php artisan app:release-feature-flags
|
||||
- run: php artisan app:release-migration-validate
|
||||
- run: php artisan app:release-shadow-compare
|
||||
- run: php artisan app:legacy-unsafe-route-audit
|
||||
- run: php artisan app:release-readiness-gate --warning-mode
|
||||
- run: php artisan test tests/Feature/Release tests/Architecture/Phase10ReleaseReadinessArchitectureTest.php
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,764 @@
|
||||
# Phase 1 Execution Plan: SchoolContext Foundation
|
||||
|
||||
|
||||
# Islamic Sunday School Alignment Update
|
||||
|
||||
Phase 1 must support **Islamic Sunday School** as a domain profile, not generic Sunday School or church-oriented Sunday School.
|
||||
|
||||
Add the domain profile value:
|
||||
|
||||
```text
|
||||
islamic_sunday_school
|
||||
```
|
||||
|
||||
`SchoolContext` may expose this profile neutrally as `domain_profile`, but `SchoolCore` services must not branch on Islamic-specific concepts directly. Any Islamic Sunday School behavior must be resolved through extension bindings, policy contracts, or context-aware providers.
|
||||
|
||||
Examples of extension-only concepts:
|
||||
|
||||
```text
|
||||
masjid_id
|
||||
program_session_id
|
||||
halaqa_id
|
||||
quran_level_id
|
||||
arabic_level_id
|
||||
islamic_studies_track_id
|
||||
volunteer_team_id
|
||||
imam_admin_note_permission
|
||||
```
|
||||
|
||||
The core still uses neutral terms: `school`, `term`, `session`, `program`, `group`, `family`, `guardian`, `student`, and `staff`.
|
||||
|
||||
---
|
||||
## 1. Objective
|
||||
|
||||
Create a mandatory, immutable `SchoolContext` foundation so every reusable service can operate safely across different school systems.
|
||||
|
||||
This phase is the base layer for the modular platform. Without it, `SchoolCore` cannot be reliably reused by Islamic Sunday School, traditional schools, training centers, after-school programs, or any other future domain. A modular platform without tenant/context isolation is just a data leak wearing a nicer folder structure.
|
||||
|
||||
## 2. Target Outcome
|
||||
|
||||
At the end of Phase 1:
|
||||
|
||||
- Every new core service receives an explicit `SchoolContext`.
|
||||
- Controllers no longer rely only on scattered `auth()`, request values, global configuration, or session-like assumptions to determine school scope.
|
||||
- Cross-school access fails by default.
|
||||
- Islamic Sunday School can resolve its own domain profile through context, without forcing Islamic Sunday School concepts into global services.
|
||||
- Finance, attendance, students, files, reporting, and communication can begin migration into `SchoolCore` safely.
|
||||
|
||||
## 3. Current Problem to Fix
|
||||
|
||||
The current app has school/year/semester context scattered across:
|
||||
|
||||
- authenticated user fields such as `school_id`, `school_year`, and `semester`;
|
||||
- global configuration access such as `Configuration::getConfig('school_year')` and `Configuration::getConfig('semester')`;
|
||||
- request query/body values;
|
||||
- controller-specific assumptions;
|
||||
- service-level fallback behavior;
|
||||
- model methods that directly read global configuration;
|
||||
- finance, attendance, reporting, and student queries that depend on string school-year/semester filters.
|
||||
|
||||
This makes the app hard to reuse for multiple school types because context is implicit instead of explicit.
|
||||
|
||||
## 4. Design Principle
|
||||
|
||||
`SchoolContext` is not a convenience object. It is a safety boundary.
|
||||
|
||||
Core rule:
|
||||
|
||||
```php
|
||||
SchoolCore services must not guess school, year, term, actor, timezone, currency, or domain profile.
|
||||
```
|
||||
|
||||
They must receive those values from an explicit context object created at the API boundary.
|
||||
|
||||
## 5. Proposed Namespace Structure
|
||||
|
||||
Create these namespaces:
|
||||
|
||||
```txt
|
||||
app/
|
||||
Domain/
|
||||
SchoolCore/
|
||||
Context/
|
||||
SchoolContext.php
|
||||
SchoolContextResolver.php
|
||||
SchoolContextFactory.php
|
||||
SchoolContextValidator.php
|
||||
SchoolContextStore.php
|
||||
SchoolContextException.php
|
||||
Contracts/
|
||||
RequiresSchoolContext.php
|
||||
Support/
|
||||
SchoolContextAware.php
|
||||
Http/
|
||||
Middleware/
|
||||
ResolveSchoolContext.php
|
||||
```
|
||||
|
||||
Optional later namespaces:
|
||||
|
||||
```txt
|
||||
app/
|
||||
Domain/
|
||||
IslamicSundaySchool/
|
||||
Context/
|
||||
IslamicSundaySchoolContextExtender.php
|
||||
```
|
||||
|
||||
Do not create the Islamic Sunday School extender in Phase 1 unless there is a concrete field or rule that cannot live in the neutral core context. Premature extension objects are how clean architecture becomes a museum of unused abstractions.
|
||||
|
||||
## 6. `SchoolContext` Value Object
|
||||
|
||||
### 6.1 Required Fields
|
||||
|
||||
Initial version:
|
||||
|
||||
```php
|
||||
final readonly class SchoolContext
|
||||
{
|
||||
public function __construct(
|
||||
public int $schoolId,
|
||||
public int $actorUserId,
|
||||
public array $actorRoleIds,
|
||||
public ?string $schoolYear,
|
||||
public ?string $term,
|
||||
public string $timezone,
|
||||
public string $locale,
|
||||
public string $currency,
|
||||
public string $domainProfile,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Field Rules
|
||||
|
||||
| Field | Required? | Purpose |
|
||||
|---|---:|---|
|
||||
| `schoolId` | Yes | Primary isolation boundary. Every scoped query must use it where schema supports it. |
|
||||
| `actorUserId` | Yes | Audit, authorization, ownership checks. |
|
||||
| `actorRoleIds` | Yes | Policy/gate checks without repeated role queries. |
|
||||
| `schoolYear` | Nullable during migration | Current academic year or legacy string value. |
|
||||
| `term` | Nullable during migration | Current term/semester. Keep name neutral in core. |
|
||||
| `timezone` | Yes | Date boundaries for attendance, deadlines, reports, finance timestamps. |
|
||||
| `locale` | Yes | Formatting, translation, report language. |
|
||||
| `currency` | Yes | Finance defaults. |
|
||||
| `domainProfile` | Yes | Example: `standard_school`, `islamic_sunday_school`, `training_center`. |
|
||||
|
||||
### 6.3 Naming Decision
|
||||
|
||||
Use `term` in the core context, not `semester`.
|
||||
|
||||
Reason: `semester` is one possible academic model. A global school platform may use quarters, trimesters, sessions, Sunday program cycles, training cohorts, or rolling terms. Use `semester` only inside legacy adapters until migration is complete.
|
||||
|
||||
## 7. Context Resolution Strategy
|
||||
|
||||
### 7.1 Resolution Priority
|
||||
|
||||
`SchoolContextResolver` should resolve context in this order:
|
||||
|
||||
1. Authenticated user from JWT/Auth.
|
||||
2. Explicit school selector from request header or route, if allowed.
|
||||
3. User default school, currently likely `users.school_id`.
|
||||
4. Current school year/term from request, if allowed and validated.
|
||||
5. Config fallback for legacy behavior, such as current `Configuration::getConfig('school_year')` and `Configuration::getConfig('semester')`.
|
||||
6. Safe defaults for timezone/locale/currency.
|
||||
|
||||
### 7.2 Recommended Request Headers
|
||||
|
||||
Support these headers during migration:
|
||||
|
||||
```txt
|
||||
X-School-Id
|
||||
X-School-Year
|
||||
X-School-Term
|
||||
X-Domain-Profile
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `X-School-Id` may only switch context if the actor belongs to that school or has platform-level permission.
|
||||
- `X-School-Year` and `X-School-Term` must be validated against available academic periods once those models exist.
|
||||
- `X-Domain-Profile` must not override server-side school configuration unless explicitly allowed for test/dev tooling.
|
||||
|
||||
## 8. Middleware Plan
|
||||
|
||||
### 8.1 Create `ResolveSchoolContext`
|
||||
|
||||
Middleware behavior:
|
||||
|
||||
```php
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$context = $this->resolver->resolve($request);
|
||||
$this->validator->assertValid($context, $request->user());
|
||||
$this->store->set($context);
|
||||
$request->attributes->set(SchoolContext::class, $context);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Middleware Order
|
||||
|
||||
Run after authentication and before authorization-sensitive controllers:
|
||||
|
||||
```txt
|
||||
ApiJwtAuth
|
||||
ResolveSchoolContext
|
||||
RequirePermission / policies / controller
|
||||
```
|
||||
|
||||
Do not run it before authentication unless supporting public routes. Public routes should either not use `SchoolContext`, or should use an explicit public context resolver with sharply limited behavior.
|
||||
|
||||
### 8.3 Route Group Rollout
|
||||
|
||||
Start with high-risk route groups only:
|
||||
|
||||
```txt
|
||||
/api/finance/*
|
||||
/api/payments/*
|
||||
/api/invoices/*
|
||||
/api/students/*
|
||||
/api/attendance/*
|
||||
/api/scanner/*
|
||||
/api/files/*
|
||||
/api/reports/*
|
||||
/api/messages/*
|
||||
/api/roles/*
|
||||
```
|
||||
|
||||
Then expand to all authenticated API routes after compatibility is verified.
|
||||
|
||||
## 9. Service Container Bindings
|
||||
|
||||
Create container bindings:
|
||||
|
||||
```php
|
||||
$this->app->singleton(SchoolContextStore::class);
|
||||
$this->app->bind(SchoolContextResolver::class);
|
||||
$this->app->bind(SchoolContextValidator::class);
|
||||
$this->app->bind(SchoolContextFactory::class);
|
||||
```
|
||||
|
||||
Add a helper only if necessary:
|
||||
|
||||
```php
|
||||
school_context(): SchoolContext
|
||||
```
|
||||
|
||||
Do not let the helper become the main service API. Core services should receive context explicitly in command DTOs or method arguments. A global helper is useful for legacy adapters, not for clean new code. Humans love turning helpers into hidden dependencies, and then act surprised when tests are brittle.
|
||||
|
||||
## 10. Service Usage Pattern
|
||||
|
||||
### 10.1 Preferred Command DTO Pattern
|
||||
|
||||
```php
|
||||
final readonly class RecordPaymentCommand
|
||||
{
|
||||
public function __construct(
|
||||
public SchoolContext $context,
|
||||
public int $invoiceId,
|
||||
public int $studentId,
|
||||
public string $method,
|
||||
public int $amountCents,
|
||||
public ?UploadedFile $file,
|
||||
public ?string $idempotencyKey,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Allowed Transitional Pattern
|
||||
|
||||
```php
|
||||
public function recordPayment(SchoolContext $context, array $payload): PaymentResult
|
||||
```
|
||||
|
||||
### 10.3 Forbidden New Pattern
|
||||
|
||||
```php
|
||||
public function recordPayment(array $payload): PaymentResult
|
||||
{
|
||||
$user = auth()->user();
|
||||
$schoolYear = Configuration::getConfig('school_year');
|
||||
}
|
||||
```
|
||||
|
||||
That pattern is legacy-only and must be marked as such.
|
||||
|
||||
## 11. Repository and Query Scoping
|
||||
|
||||
### 11.1 Scope Helper
|
||||
|
||||
Create a reusable query helper or trait:
|
||||
|
||||
```php
|
||||
trait AppliesSchoolContext
|
||||
{
|
||||
protected function applySchoolScope(Builder $query, SchoolContext $context, string $column = 'school_id'): Builder
|
||||
{
|
||||
return $query->where($column, $context->schoolId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 Legacy Schema Handling
|
||||
|
||||
Some tables may not have `school_id`. For Phase 1, classify tables into:
|
||||
|
||||
| Type | Handling |
|
||||
|---|---|
|
||||
| Has `school_id` | Apply strict `school_id` filter. |
|
||||
| Has academic year/term only | Apply `schoolYear` and `term` filters as legacy scope. |
|
||||
| Global lookup table | Explicitly mark as global. |
|
||||
| Missing required scope | Add to schema remediation backlog. |
|
||||
|
||||
Do not silently use `Schema::hasColumn()` inside business logic forever. Temporary schema detection is acceptable only inside migration adapters with comments and removal tickets.
|
||||
|
||||
## 12. Migration Strategy
|
||||
|
||||
### Step 1: Add classes without changing behavior
|
||||
|
||||
Create:
|
||||
|
||||
- `SchoolContext`
|
||||
- `SchoolContextFactory`
|
||||
- `SchoolContextResolver`
|
||||
- `SchoolContextValidator`
|
||||
- `SchoolContextStore`
|
||||
- `ResolveSchoolContext` middleware
|
||||
|
||||
Add tests for object creation and resolution.
|
||||
|
||||
### Step 2: Resolve context for authenticated routes
|
||||
|
||||
Enable middleware in a small route group first. Recommended first group:
|
||||
|
||||
```txt
|
||||
finance/payment read-only endpoint or student read-only endpoint
|
||||
```
|
||||
|
||||
Avoid starting with payment mutation until context resolution has tests. Bravery is not a rollout strategy.
|
||||
|
||||
### Step 3: Add context to audit/logging
|
||||
|
||||
Every high-risk action should have actor and school context available, even before full service refactor.
|
||||
|
||||
Add to logs where possible:
|
||||
|
||||
```txt
|
||||
school_id
|
||||
actor_user_id
|
||||
school_year
|
||||
term
|
||||
domain_profile
|
||||
request_id
|
||||
```
|
||||
|
||||
### Step 4: Convert first vertical slice
|
||||
|
||||
Choose one low-risk read path first:
|
||||
|
||||
```txt
|
||||
Student list/read
|
||||
```
|
||||
|
||||
Then one high-risk mutation path:
|
||||
|
||||
```txt
|
||||
Manual payment record/edit after tests exist
|
||||
```
|
||||
|
||||
### Step 5: Enforce context in new services
|
||||
|
||||
Any new `SchoolCore` service must accept context. No exceptions except explicitly global lookup services.
|
||||
|
||||
## 13. First Vertical Slice: Student Read Context
|
||||
|
||||
Use student read/list endpoints as the first validation slice because they are easier to test than finance mutations.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Add `ResolveSchoolContext` to student read route group.
|
||||
- Inject/request `SchoolContext` in controller.
|
||||
- Pass context to student query service.
|
||||
- Apply `school_id` scope where available.
|
||||
- If student table lacks correct school scoping, document schema gap.
|
||||
- Add wrong-school read test.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- User from School A can list School A students.
|
||||
- User from School A cannot list School B students.
|
||||
- Missing/invalid context returns `403` or `422`, not a silent fallback.
|
||||
- Existing valid Islamic Sunday School requests still work.
|
||||
|
||||
## 14. Second Vertical Slice: Payment File Access Context
|
||||
|
||||
This should connect to the P0 fix already identified.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Add context to payment file download route.
|
||||
- Change route from filename-only to payment/file-id-based access.
|
||||
- Load payment under `school_id` context.
|
||||
- Authorize actor against payment/invoice/family/finance role.
|
||||
- Resolve physical file only from stored payment record.
|
||||
- Log access attempt with context.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Finance/admin from same school can download payment file.
|
||||
- Parent/guardian can download only if policy allows and relationship matches.
|
||||
- User from another school cannot download even if they know the filename.
|
||||
- Unknown payment returns `404`.
|
||||
- Known payment with unauthorized actor returns `403`.
|
||||
|
||||
## 15. Validation Rules
|
||||
|
||||
`SchoolContextValidator` must check:
|
||||
|
||||
- actor user exists;
|
||||
- actor is active/not suspended;
|
||||
- `schoolId` is positive;
|
||||
- actor belongs to selected school;
|
||||
- selected school is active;
|
||||
- selected year/term is allowed for selected school when academic-period tables are available;
|
||||
- timezone is valid IANA timezone;
|
||||
- locale is supported;
|
||||
- currency is valid ISO currency code;
|
||||
- domain profile is supported.
|
||||
|
||||
During migration, when academic period tables are not yet reliable, log warnings instead of blocking all requests. Do not pretend this is perfect. Transitional systems lie unless you make the lies visible.
|
||||
|
||||
## 16. Authorization Integration
|
||||
|
||||
Policies and gates should receive or resolve context.
|
||||
|
||||
Preferred:
|
||||
|
||||
```php
|
||||
$this->authorize('view', [$student, $context]);
|
||||
```
|
||||
|
||||
Alternative transitional behavior:
|
||||
|
||||
```php
|
||||
$context = app(SchoolContextStore::class)->current();
|
||||
$this->authorize('view', $student);
|
||||
```
|
||||
|
||||
Policy checks must compare resource scope against context:
|
||||
|
||||
```php
|
||||
return $student->school_id === $context->schoolId;
|
||||
```
|
||||
|
||||
If a model does not have `school_id`, the policy must use a documented relationship path.
|
||||
|
||||
## 17. Jobs, Events, and Notifications
|
||||
|
||||
Any job created from a scoped action must carry context values explicitly.
|
||||
|
||||
### Required Job Fields
|
||||
|
||||
```txt
|
||||
school_id
|
||||
actor_user_id
|
||||
school_year
|
||||
term
|
||||
domain_profile
|
||||
locale
|
||||
timezone
|
||||
request_id or correlation_id
|
||||
```
|
||||
|
||||
Do not let queued jobs recalculate context from the current logged-in user. There is no current logged-in user in a worker. This is apparently easy to forget until production sends the wrong family a message.
|
||||
|
||||
## 18. Files and Storage
|
||||
|
||||
File services must receive context.
|
||||
|
||||
Recommended storage path pattern:
|
||||
|
||||
```txt
|
||||
schools/{school_id}/{module}/{resource_type}/{resource_id}/{filename}
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```txt
|
||||
schools/12/payments/checks/8821/check_8821.pdf
|
||||
schools/12/students/documents/440/immunization.pdf
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- never serve files by arbitrary filename alone;
|
||||
- never infer authorization from path;
|
||||
- always authorize resource first;
|
||||
- path resolution happens after authorization;
|
||||
- logs include context and resource ID.
|
||||
|
||||
## 19. Reporting
|
||||
|
||||
Reports must include context in every query.
|
||||
|
||||
Minimum rules:
|
||||
|
||||
- every report query must accept `SchoolContext`;
|
||||
- report export filenames should include school/year/term where appropriate;
|
||||
- cross-school report access must fail;
|
||||
- raw SQL reports must bind context parameters explicitly.
|
||||
|
||||
## 20. Test Plan
|
||||
|
||||
### 20.1 Unit Tests
|
||||
|
||||
Create tests for:
|
||||
|
||||
- `SchoolContext` construction;
|
||||
- invalid school ID rejection;
|
||||
- invalid timezone rejection;
|
||||
- invalid currency rejection;
|
||||
- resolver reads authenticated user school;
|
||||
- resolver rejects unauthorized `X-School-Id` switch;
|
||||
- resolver applies legacy school year/term fallback;
|
||||
- context store returns current context;
|
||||
- context store throws when missing context.
|
||||
|
||||
### 20.2 Feature Tests
|
||||
|
||||
Create tests for:
|
||||
|
||||
- authenticated route receives context;
|
||||
- unauthenticated route cannot resolve protected context;
|
||||
- School A user cannot access School B student;
|
||||
- School A finance user cannot access School B payment file;
|
||||
- `X-School-Id` override works only for authorized multi-school/platform actor;
|
||||
- timezone affects attendance date boundary;
|
||||
- context is logged for high-risk mutation.
|
||||
|
||||
### 20.3 Architecture Tests
|
||||
|
||||
Add static tests/rules:
|
||||
|
||||
```txt
|
||||
SchoolCore services must not call auth()
|
||||
SchoolCore services must not read Request directly
|
||||
SchoolCore services must not call Configuration::getConfig directly
|
||||
SchoolCore services must accept SchoolContext for scoped operations
|
||||
SchoolCore must not import IslamicSundaySchool namespace
|
||||
```
|
||||
|
||||
## 21. Implementation Tickets
|
||||
|
||||
### SCX-001: Create `SchoolContext` value object
|
||||
|
||||
**Goal:** Define immutable context object used by core services.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `App\Domain\SchoolCore\Context\SchoolContext`.
|
||||
- Add constructor fields listed in this plan.
|
||||
- Add helper methods:
|
||||
- `isIslamicSundaySchool(): bool`
|
||||
- `requiresTerm(): bool`
|
||||
- `auditPayload(): array`
|
||||
- `withTerm(?string $term): self`
|
||||
- Add unit tests.
|
||||
|
||||
**Done when:** object is immutable, tested, and contains no request/auth dependencies.
|
||||
|
||||
### SCX-002: Create context resolver and factory
|
||||
|
||||
**Goal:** Build context from authenticated request safely.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `SchoolContextFactory`.
|
||||
- Create `SchoolContextResolver`.
|
||||
- Read user, headers, legacy config, and defaults.
|
||||
- Validate switch permissions for `X-School-Id`.
|
||||
- Return deterministic context.
|
||||
|
||||
**Done when:** resolver works for normal user, admin user, invalid school, and missing school cases.
|
||||
|
||||
### SCX-003: Create context validator
|
||||
|
||||
**Goal:** Prevent invalid or unauthorized context.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `SchoolContextValidator`.
|
||||
- Validate actor belongs to selected school.
|
||||
- Validate school active state where model exists.
|
||||
- Validate timezone/locale/currency/domain profile.
|
||||
- Add migration warnings for missing academic period tables.
|
||||
|
||||
**Done when:** invalid contexts fail before controller logic executes.
|
||||
|
||||
### SCX-004: Add middleware
|
||||
|
||||
**Goal:** Attach context to authenticated API requests.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `ResolveSchoolContext` middleware.
|
||||
- Register middleware alias.
|
||||
- Add to selected route groups after auth.
|
||||
- Store context in request attributes and `SchoolContextStore`.
|
||||
|
||||
**Done when:** protected routes can retrieve context and tests prove middleware order.
|
||||
|
||||
### SCX-005: Add query scoping helpers
|
||||
|
||||
**Goal:** Make school scoping repeatable.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `AppliesSchoolContext` trait or query helper.
|
||||
- Add strict school scope behavior.
|
||||
- Add legacy year/term scope behavior.
|
||||
- Add table classification list for global vs scoped tables.
|
||||
|
||||
**Done when:** first read service uses helper and wrong-school reads fail.
|
||||
|
||||
### SCX-006: Convert first student read/list slice
|
||||
|
||||
**Goal:** Prove context works on a low-risk endpoint.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Add context middleware to selected student route.
|
||||
- Pass context to student query service.
|
||||
- Scope query by school.
|
||||
- Add cross-school tests.
|
||||
|
||||
**Done when:** same-school student reads pass and cross-school reads fail.
|
||||
|
||||
### SCX-007: Convert payment file access slice
|
||||
|
||||
**Goal:** Use context on a high-risk P0 issue.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Replace filename-only access with payment-id-based access.
|
||||
- Load payment using context.
|
||||
- Authorize resource before file path resolution.
|
||||
- Add logs and tests.
|
||||
|
||||
**Done when:** guessed filenames cannot access files and cross-school access fails.
|
||||
|
||||
### SCX-008: Add job/event context payload convention
|
||||
|
||||
**Goal:** Prevent async work from losing school/actor context.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Define `ContextPayload` array format.
|
||||
- Add helper to create payload from `SchoolContext`.
|
||||
- Update one notification or payment event as pilot.
|
||||
- Add tests.
|
||||
|
||||
**Done when:** queued pilot job does not depend on live auth/session/request state.
|
||||
|
||||
### SCX-009: Add architecture enforcement
|
||||
|
||||
**Goal:** Stop new code from bypassing context.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Add static/architecture tests.
|
||||
- Ban `auth()`, `request()`, and direct `Configuration::getConfig()` inside new `SchoolCore` services.
|
||||
- Ban `SchoolCore -> IslamicSundaySchool` imports.
|
||||
|
||||
**Done when:** CI fails on forbidden dependency direction or hidden context access.
|
||||
|
||||
## 22. Rollout Timeline by Workstream
|
||||
|
||||
### Workstream A: Foundation
|
||||
|
||||
1. `SchoolContext`
|
||||
2. Factory/resolver/validator
|
||||
3. Middleware/store
|
||||
4. Tests
|
||||
|
||||
### Workstream B: First Usage
|
||||
|
||||
1. Student read/list
|
||||
2. Payment file access
|
||||
3. Context-aware logging
|
||||
|
||||
### Workstream C: Enforcement
|
||||
|
||||
1. Static rules
|
||||
2. Architecture tests
|
||||
3. Migration checklist for all future services
|
||||
|
||||
## 23. Acceptance Criteria for Phase 1
|
||||
|
||||
Phase 1 is complete only when all of this is true:
|
||||
|
||||
- `SchoolContext` exists and is immutable.
|
||||
- Context is resolved for selected authenticated API routes.
|
||||
- Context includes school, actor, year/term, timezone, locale, currency, and domain profile.
|
||||
- Cross-school student read test passes.
|
||||
- Cross-school payment file access test passes.
|
||||
- At least one read service and one high-risk access path use explicit context.
|
||||
- New `SchoolCore` services are forbidden from reading request/auth/config directly.
|
||||
- `SchoolCore` cannot depend on `IslamicSundaySchool` namespace.
|
||||
- Islamic Sunday School still works through context profile and legacy adapters.
|
||||
- Known schema gaps are documented instead of silently hidden.
|
||||
|
||||
## 24. Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|---|---:|---|
|
||||
| Tables lack `school_id` | Cross-school isolation may be incomplete | Classify tables and add schema remediation tickets. |
|
||||
| Legacy code depends on global config | Context behavior may differ from old behavior | Use resolver fallback during migration and log warnings. |
|
||||
| Too much migration at once | High regression risk | Start with student read/list, then payment file access. |
|
||||
| Developers bypass context | Architecture decay | Add static rules and CI checks. |
|
||||
| Islamic Sunday School assumptions leak into core | Platform becomes non-reusable | Enforce dependency direction and neutral vocabulary. |
|
||||
|
||||
## 25. Non-Goals
|
||||
|
||||
Phase 1 does not include:
|
||||
|
||||
- moving every service into `SchoolCore`;
|
||||
- fully refactoring finance;
|
||||
- fully refactoring attendance;
|
||||
- replacing all `semester` fields;
|
||||
- adding a full multi-tenant school administration UI;
|
||||
- converting every controller;
|
||||
- renaming every legacy table.
|
||||
|
||||
Trying to do all of that in Phase 1 would be how a refactor becomes a bonfire.
|
||||
|
||||
## 26. Immediate Next Implementation Order
|
||||
|
||||
Use this order:
|
||||
|
||||
```txt
|
||||
1. Create SchoolContext value object
|
||||
2. Create resolver/factory/validator/store
|
||||
3. Add middleware after ApiJwtAuth
|
||||
4. Add unit tests for resolver and validator
|
||||
5. Add student read/list context slice
|
||||
6. Add cross-school student tests
|
||||
7. Add payment file context slice
|
||||
8. Add cross-school payment file tests
|
||||
9. Add architecture rules
|
||||
10. Document remaining schema gaps
|
||||
```
|
||||
|
||||
## 27. Definition of Success
|
||||
|
||||
A developer should be able to write this in a future core service:
|
||||
|
||||
```php
|
||||
public function listStudents(SchoolContext $context, StudentListQuery $query): StudentListResult
|
||||
```
|
||||
|
||||
And the service should not need to know whether the school is a Islamic Sunday School, a private academy, a tutoring center, or a training program.
|
||||
|
||||
That is the point. The global platform owns the universal school workflow. Islamic Sunday School supplies policies, labels, calendars, and domain-specific extensions. The core stays boring, reusable, and annoyingly difficult to misuse. Good architecture is mostly removing opportunities for future nonsense.
|
||||
@@ -0,0 +1,942 @@
|
||||
# Phase 2 Execution Plan: Core Contracts and Module Boundaries
|
||||
|
||||
|
||||
# Islamic Sunday School Alignment Update
|
||||
|
||||
Phase 2 must define contracts that support **Islamic Sunday School** through extension implementations, not by adding Islamic-specific language to the core interfaces.
|
||||
|
||||
Preferred extension namespace:
|
||||
|
||||
```text
|
||||
App\Domain\IslamicSundaySchool
|
||||
```
|
||||
|
||||
The core contracts remain school-neutral. Islamic Sunday School implementations may bind policies for:
|
||||
|
||||
```text
|
||||
Qur'an progression
|
||||
Arabic level placement
|
||||
Islamic studies tracks
|
||||
halaqa/class grouping
|
||||
masjid family/community profile
|
||||
program-session attendance
|
||||
volunteer team communication
|
||||
tuition, sadaqah, scholarship, and fee classification
|
||||
```
|
||||
|
||||
Do not put these terms in `SchoolCore` contracts. The contracts define capabilities. The extension defines Islamic Sunday School behavior.
|
||||
|
||||
---
|
||||
## Purpose
|
||||
|
||||
Phase 2 establishes the contract layer for the modular school platform. The goal is to define stable interfaces for shared school operations before moving implementation logic into `SchoolCore`.
|
||||
|
||||
This phase prevents the platform from becoming Sunday-School-shaped by accident. Islamic Sunday School may extend the global platform, but the global platform must not know Islamic Sunday School exists.
|
||||
|
||||
## Phase 2 Outcome
|
||||
|
||||
At the end of this phase:
|
||||
|
||||
- `SchoolCore` exposes stable contracts for core school operations.
|
||||
- Islamic Sunday School behavior plugs into the platform through policies, providers, and extension services.
|
||||
- Controllers and application services begin depending on contracts instead of concrete legacy services.
|
||||
- Dependency direction is enforced by automated tests.
|
||||
- No `SchoolCore` class imports from `IslamicSundaySchool`.
|
||||
- New implementation work has a clear place to go instead of continuing the existing service sprawl.
|
||||
|
||||
## Dependency Rule
|
||||
|
||||
The dependency direction must be enforced as architecture law:
|
||||
|
||||
```text
|
||||
Http / Controllers / Requests / Resources
|
||||
↓
|
||||
Application Services / Use Cases
|
||||
↓
|
||||
SchoolCore Contracts
|
||||
↓
|
||||
SchoolCore Implementations
|
||||
↑
|
||||
Domain Extensions, such as IslamicSundaySchool, may provide policies/providers
|
||||
```
|
||||
|
||||
Allowed:
|
||||
|
||||
```text
|
||||
IslamicSundaySchool → SchoolCore
|
||||
Controllers → SchoolCore Contracts
|
||||
Application Services → SchoolCore Contracts
|
||||
SchoolCore Implementations → SchoolCore Models / Repositories
|
||||
```
|
||||
|
||||
Forbidden:
|
||||
|
||||
```text
|
||||
SchoolCore → IslamicSundaySchool
|
||||
SchoolCore → Http Request
|
||||
SchoolCore → auth()
|
||||
SchoolCore → controller classes
|
||||
SchoolCore → route-specific assumptions
|
||||
SchoolCore → Islamic Sunday School program calendar assumptions
|
||||
SchoolCore → masjid/community/ministry vocabulary unless stored as extension metadata
|
||||
```
|
||||
|
||||
## Required Namespace Structure
|
||||
|
||||
Create this structure first. Do not migrate every class immediately. Empty boundaries are acceptable at the start. Fake modularity is not.
|
||||
|
||||
```text
|
||||
app/
|
||||
Domain/
|
||||
SchoolCore/
|
||||
Contracts/
|
||||
Context/
|
||||
Identity/
|
||||
Students/
|
||||
Guardians/
|
||||
Enrollment/
|
||||
Attendance/
|
||||
Academics/
|
||||
Finance/
|
||||
Communication/
|
||||
Files/
|
||||
Reporting/
|
||||
Authorization/
|
||||
Support/
|
||||
IslamicSundaySchool/
|
||||
Contracts/
|
||||
Providers/
|
||||
Policies/
|
||||
Attendance/
|
||||
Curriculum/
|
||||
Ministry/
|
||||
Families/
|
||||
Reports/
|
||||
Shared/
|
||||
Contracts/
|
||||
ValueObjects/
|
||||
Exceptions/
|
||||
Support/
|
||||
Providers/
|
||||
SchoolCoreServiceProvider.php
|
||||
IslamicSundaySchoolServiceProvider.php
|
||||
```
|
||||
|
||||
## Contract Design Rules
|
||||
|
||||
Every contract must follow these rules:
|
||||
|
||||
1. Accept `SchoolContext` explicitly or be resolved from a context-aware application service.
|
||||
2. Use DTOs/value objects for input where payloads are complex.
|
||||
3. Return DTOs/read models where behavior crosses module boundaries.
|
||||
4. Never accept raw `Request` objects.
|
||||
5. Never call `auth()` inside domain services.
|
||||
6. Never use Islamic Sunday School terminology in `SchoolCore` contracts.
|
||||
7. Never expose Eloquent models as the required public contract unless the service is strictly internal.
|
||||
8. Define authorization expectations explicitly.
|
||||
9. Define transaction ownership explicitly for mutation methods.
|
||||
10. Include failure behavior in the contract documentation.
|
||||
|
||||
## Core Contracts to Create
|
||||
|
||||
### Context
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface RequiresSchoolContext
|
||||
{
|
||||
public function setSchoolContext(SchoolContext $context): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Students
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Students\Data\CreateStudentData;
|
||||
use App\Domain\SchoolCore\Students\Data\UpdateStudentData;
|
||||
use App\Domain\SchoolCore\Students\Data\StudentReadModel;
|
||||
|
||||
interface StudentServiceContract
|
||||
{
|
||||
public function create(SchoolContext $context, CreateStudentData $data): StudentReadModel;
|
||||
|
||||
public function update(SchoolContext $context, int $studentId, UpdateStudentData $data): StudentReadModel;
|
||||
|
||||
public function find(SchoolContext $context, int $studentId): ?StudentReadModel;
|
||||
|
||||
public function archive(SchoolContext $context, int $studentId, string $reason): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Student Identifier Generation
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface StudentIdentifierGeneratorContract
|
||||
{
|
||||
public function generate(SchoolContext $context, int $studentId): string;
|
||||
}
|
||||
```
|
||||
|
||||
### Guardians
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Guardians\Data\GuardianRelationshipData;
|
||||
|
||||
interface GuardianServiceContract
|
||||
{
|
||||
public function attachGuardian(SchoolContext $context, int $studentId, GuardianRelationshipData $data): void;
|
||||
|
||||
public function detachGuardian(SchoolContext $context, int $studentId, int $guardianId): void;
|
||||
|
||||
public function listForStudent(SchoolContext $context, int $studentId): array;
|
||||
}
|
||||
```
|
||||
|
||||
### Enrollment
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Enrollment\Data\EnrollmentData;
|
||||
|
||||
interface EnrollmentServiceContract
|
||||
{
|
||||
public function enroll(SchoolContext $context, int $studentId, EnrollmentData $data): void;
|
||||
|
||||
public function transfer(SchoolContext $context, int $studentId, EnrollmentData $data): void;
|
||||
|
||||
public function withdraw(SchoolContext $context, int $studentId, string $reason): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Attendance
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Attendance\Data\AttendanceMarkData;
|
||||
use App\Domain\SchoolCore\Attendance\Data\AttendanceScanData;
|
||||
use App\Domain\SchoolCore\Attendance\Data\AttendanceResult;
|
||||
|
||||
interface AttendanceServiceContract
|
||||
{
|
||||
public function markStudent(SchoolContext $context, AttendanceMarkData $data): AttendanceResult;
|
||||
|
||||
public function processScan(SchoolContext $context, AttendanceScanData $data): AttendanceResult;
|
||||
|
||||
public function summarizeDay(SchoolContext $context, string $date): array;
|
||||
}
|
||||
```
|
||||
|
||||
### Attendance Policy
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use DateTimeInterface;
|
||||
|
||||
interface AttendancePolicyContract
|
||||
{
|
||||
public function isSchoolDay(SchoolContext $context, DateTimeInterface $date): bool;
|
||||
|
||||
public function determineStatus(SchoolContext $context, DateTimeInterface $scanTime, array $session): string;
|
||||
|
||||
public function duplicateScanWindowSeconds(SchoolContext $context): int;
|
||||
}
|
||||
```
|
||||
|
||||
### Academic Calendar
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use DateTimeInterface;
|
||||
|
||||
interface AcademicCalendarProviderContract
|
||||
{
|
||||
public function currentAcademicYear(SchoolContext $context): ?int;
|
||||
|
||||
public function currentTerm(SchoolContext $context): ?int;
|
||||
|
||||
public function sessionsForDate(SchoolContext $context, DateTimeInterface $date): array;
|
||||
}
|
||||
```
|
||||
|
||||
### Academics
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface GradeScalePolicyContract
|
||||
{
|
||||
public function gradeForScore(SchoolContext $context, float $score): string;
|
||||
|
||||
public function passingScore(SchoolContext $context): float;
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface PromotionPolicyContract
|
||||
{
|
||||
public function canPromote(SchoolContext $context, int $studentId): bool;
|
||||
|
||||
public function nextPlacement(SchoolContext $context, int $studentId): array;
|
||||
}
|
||||
```
|
||||
|
||||
### Finance
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Finance\Data\CreateInvoiceData;
|
||||
use App\Domain\SchoolCore\Finance\Data\InvoiceReadModel;
|
||||
|
||||
interface InvoiceServiceContract
|
||||
{
|
||||
public function create(SchoolContext $context, CreateInvoiceData $data): InvoiceReadModel;
|
||||
|
||||
public function recalculate(SchoolContext $context, int $invoiceId): InvoiceReadModel;
|
||||
|
||||
public function find(SchoolContext $context, int $invoiceId): ?InvoiceReadModel;
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Finance\Data\RecordPaymentData;
|
||||
use App\Domain\SchoolCore\Finance\Data\EditPaymentData;
|
||||
use App\Domain\SchoolCore\Finance\Data\PaymentReadModel;
|
||||
|
||||
interface PaymentServiceContract
|
||||
{
|
||||
public function record(SchoolContext $context, RecordPaymentData $data): PaymentReadModel;
|
||||
|
||||
public function edit(SchoolContext $context, int $paymentId, EditPaymentData $data): PaymentReadModel;
|
||||
|
||||
public function void(SchoolContext $context, int $paymentId, string $reason): void;
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface PaymentAllocationPolicyContract
|
||||
{
|
||||
public function allocate(SchoolContext $context, int $invoiceId, float $amount): array;
|
||||
}
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface FileAccessPolicyContract
|
||||
{
|
||||
public function canAccess(SchoolContext $context, string $filePurpose, int $ownerId, array $metadata = []): bool;
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface FileStorageServiceContract
|
||||
{
|
||||
public function store(SchoolContext $context, string $purpose, mixed $file, array $metadata = []): string;
|
||||
|
||||
public function resolvePath(SchoolContext $context, string $fileReference): string;
|
||||
|
||||
public function delete(SchoolContext $context, string $fileReference): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Communication
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Communication\Data\MessageData;
|
||||
use App\Domain\SchoolCore\Communication\Data\RecipientQuery;
|
||||
|
||||
interface CommunicationServiceContract
|
||||
{
|
||||
public function previewRecipients(SchoolContext $context, RecipientQuery $query): array;
|
||||
|
||||
public function send(SchoolContext $context, MessageData $message): array;
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
use App\Domain\SchoolCore\Communication\Data\RecipientQuery;
|
||||
|
||||
interface RecipientResolverContract
|
||||
{
|
||||
public function resolve(SchoolContext $context, RecipientQuery $query): array;
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface CommunicationPreferencePolicyContract
|
||||
{
|
||||
public function mayContact(SchoolContext $context, int $recipientId, string $channel, string $purpose): bool;
|
||||
}
|
||||
```
|
||||
|
||||
### Reporting
|
||||
|
||||
```php
|
||||
namespace App\Domain\SchoolCore\Contracts;
|
||||
|
||||
use App\Domain\SchoolCore\Context\SchoolContext;
|
||||
|
||||
interface ReportServiceContract
|
||||
{
|
||||
public function generate(SchoolContext $context, string $reportKey, array $filters = []): array;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO and Read Model Rules
|
||||
|
||||
Create DTOs only where contracts need them. Do not invent DTOs for every two-field method just to feel enterprise.
|
||||
|
||||
Recommended naming:
|
||||
|
||||
```text
|
||||
CreateStudentData
|
||||
UpdateStudentData
|
||||
StudentReadModel
|
||||
AttendanceMarkData
|
||||
AttendanceScanData
|
||||
AttendanceResult
|
||||
CreateInvoiceData
|
||||
RecordPaymentData
|
||||
EditPaymentData
|
||||
PaymentReadModel
|
||||
MessageData
|
||||
RecipientQuery
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- DTOs must be immutable where practical.
|
||||
- DTOs must be created from validated request data, not raw request data.
|
||||
- DTOs must not call the database.
|
||||
- Read models must not expose sensitive fields by default.
|
||||
- Read models must include `school_id` only when needed for authorization/debugging, not casually leaked in every response.
|
||||
|
||||
## Service Provider Bindings
|
||||
|
||||
Create `SchoolCoreServiceProvider`.
|
||||
|
||||
```php
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Domain\SchoolCore\Contracts\StudentServiceContract;
|
||||
use App\Domain\SchoolCore\Students\Services\StudentService;
|
||||
|
||||
class SchoolCoreServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(StudentServiceContract::class, StudentService::class);
|
||||
// Bind additional core contracts as implementations are introduced.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `IslamicSundaySchoolServiceProvider` for extension policies.
|
||||
|
||||
```php
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Domain\SchoolCore\Contracts\AttendancePolicyContract;
|
||||
use App\Domain\IslamicIslamicSundaySchool\Attendance\IslamicSundaySchoolAttendancePolicy;
|
||||
|
||||
class IslamicSundaySchoolServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(AttendancePolicyContract::class, IslamicSundaySchoolAttendancePolicy::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Important: core contract bindings may point to core defaults. Islamic Sunday School may override only extension contracts/policies, not replace the entire platform unless a module-specific reason is documented.
|
||||
|
||||
## Binding Strategy
|
||||
|
||||
Use this three-level binding model:
|
||||
|
||||
### 1. Core service contracts
|
||||
|
||||
These are stable platform services.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
StudentServiceContract → SchoolCore\Students\Services\StudentService
|
||||
PaymentServiceContract → SchoolCore\Finance\Services\PaymentService
|
||||
AttendanceServiceContract → SchoolCore\Attendance\Services\AttendanceService
|
||||
```
|
||||
|
||||
### 2. Policy/provider contracts
|
||||
|
||||
These are extension points.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
AttendancePolicyContract → IslamicSundaySchool\Attendance\IslamicSundaySchoolAttendancePolicy
|
||||
AcademicCalendarProviderContract → IslamicSundaySchool\Providers\IslamicSundaySchoolCalendarProvider
|
||||
PaymentAllocationPolicyContract → IslamicSundaySchool\Policies\IslamicSundaySchoolPaymentAllocationPolicy
|
||||
```
|
||||
|
||||
### 3. Infrastructure contracts
|
||||
|
||||
These provide storage, messaging, and integration adapters.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
FileStorageServiceContract → LaravelLocalFileStorageService
|
||||
CommunicationServiceContract → LaravelCommunicationService
|
||||
```
|
||||
|
||||
## First Migration Slice: Student Identifier Generation
|
||||
|
||||
This is the safest first slice because it fixes a real design bug without moving the whole student module.
|
||||
|
||||
### Current Problem
|
||||
|
||||
Student creation currently risks race conditions when identifiers are generated using `max(id) + 1` before the student row is safely persisted.
|
||||
|
||||
### Target Design
|
||||
|
||||
```text
|
||||
StudentController
|
||||
→ CreateStudentRequest
|
||||
→ CreateStudentData
|
||||
→ StudentServiceContract
|
||||
→ StudentService
|
||||
→ StudentIdentifierGeneratorContract
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Create `StudentIdentifierGeneratorContract`.
|
||||
2. Create default implementation: `SequentialStudentIdentifierGenerator`.
|
||||
3. Generate identifier after student insert using the persisted student ID.
|
||||
4. Add unique database constraint for `(school_id, school_id_number)` or equivalent identifier column.
|
||||
5. Add retry behavior for collisions.
|
||||
6. Add concurrent creation test.
|
||||
7. Keep the existing API response compatible.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Two concurrent student creations cannot produce the same identifier.
|
||||
- Student ID format can be changed by binding a different generator.
|
||||
- Islamic Sunday School-specific identifier formatting does not live inside core student creation logic.
|
||||
- Existing student creation endpoint still works.
|
||||
|
||||
## Second Migration Slice: Payment File Access Policy
|
||||
|
||||
This slice validates that contracts can enforce security boundaries.
|
||||
|
||||
### Current Problem
|
||||
|
||||
Filename-based file serving risks unauthorized access if a user can guess a payment file name.
|
||||
|
||||
### Target Design
|
||||
|
||||
```text
|
||||
PaymentManualController
|
||||
→ PaymentFileDownloadRequest
|
||||
→ PaymentServiceContract or PaymentFileService
|
||||
→ FileAccessPolicyContract
|
||||
→ FileStorageServiceContract
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Create `FileAccessPolicyContract`.
|
||||
2. Create `PaymentFileAccessPolicy` implementation.
|
||||
3. Replace filename-only lookup with payment-ID-based lookup.
|
||||
4. Resolve file path from the payment record.
|
||||
5. Authorize against school context and actor permissions.
|
||||
6. Add audit log entry for file access.
|
||||
7. Return `403` for unauthorized access.
|
||||
8. Return `404` for missing file or missing payment.
|
||||
9. Add tests for guessed filename access.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Users cannot download payment files by guessing filenames.
|
||||
- Finance/admin users can download authorized payment files.
|
||||
- Parents can only download files tied to their own financial records, if product rules allow parent access.
|
||||
- Cross-school access fails.
|
||||
- Payment file access uses `SchoolContext`.
|
||||
|
||||
## Third Migration Slice: Attendance Policy Stub
|
||||
|
||||
This slice prepares Phase 4 without fully extracting scanner logic yet.
|
||||
|
||||
### Target Design
|
||||
|
||||
```text
|
||||
ScannerController
|
||||
→ AttendanceServiceContract
|
||||
→ AttendancePolicyContract
|
||||
→ AcademicCalendarProviderContract
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Create `AttendancePolicyContract`.
|
||||
2. Create default `StandardSchoolAttendancePolicy`.
|
||||
3. Create `IslamicSundaySchoolAttendancePolicy`.
|
||||
4. Create `AcademicCalendarProviderContract`.
|
||||
5. Create default provider.
|
||||
6. Bind Islamic Sunday School provider in `IslamicSundaySchoolServiceProvider`.
|
||||
7. Update scanner service extraction plan to call policies instead of hardcoded date/session assumptions.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Islamic Sunday School program attendance logic lives outside core.
|
||||
- Attendance duplicate window is policy-driven.
|
||||
- Scanner code can call a policy without knowing the school type.
|
||||
|
||||
## Controller Dependency Rule
|
||||
|
||||
New or touched controllers should depend on contracts, not concrete domain services.
|
||||
|
||||
Good:
|
||||
|
||||
```php
|
||||
public function __construct(private StudentServiceContract $students) {}
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```php
|
||||
public function __construct(private IslamicSundaySchoolStudentService $students) {}
|
||||
```
|
||||
|
||||
Also bad:
|
||||
|
||||
```php
|
||||
public function store(Request $request)
|
||||
{
|
||||
$schoolId = auth()->user()->school_id;
|
||||
// 120 lines of student creation logic
|
||||
}
|
||||
```
|
||||
|
||||
Because apparently controllers become landfill if left unsupervised.
|
||||
|
||||
## Architecture Tests
|
||||
|
||||
Add architecture tests using one of these approaches:
|
||||
|
||||
- Pest architecture tests if Pest is available.
|
||||
- PHPUnit custom tests scanning namespaces/imports.
|
||||
- Static analysis rule in PHPStan/Larastan if already installed.
|
||||
|
||||
### Required Architecture Tests
|
||||
|
||||
#### `SchoolCore` must not import `IslamicSundaySchool`
|
||||
|
||||
```php
|
||||
public function test_school_core_does_not_depend_on_islamic_sunday_school(): void
|
||||
{
|
||||
$files = glob(app_path('Domain/SchoolCore/**/*.php'));
|
||||
|
||||
foreach ($files as $file) {
|
||||
$contents = file_get_contents($file);
|
||||
$this->assertStringNotContainsString('App\\Domain\\IslamicSundaySchool', $contents, $file);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Domain services must not use raw request
|
||||
|
||||
```php
|
||||
public function test_domain_services_do_not_depend_on_http_request(): void
|
||||
{
|
||||
$files = glob(app_path('Domain/**/*.php'));
|
||||
|
||||
foreach ($files as $file) {
|
||||
$contents = file_get_contents($file);
|
||||
$this->assertStringNotContainsString('Illuminate\\Http\\Request', $contents, $file);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Domain services must not call `auth()`
|
||||
|
||||
```php
|
||||
public function test_domain_services_do_not_call_auth_helper(): void
|
||||
{
|
||||
$files = glob(app_path('Domain/**/*.php'));
|
||||
|
||||
foreach ($files as $file) {
|
||||
$contents = file_get_contents($file);
|
||||
$this->assertStringNotContainsString('auth()', $contents, $file);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Core contracts must be neutral
|
||||
|
||||
Search contract names and method names for Islamic Sunday School vocabulary:
|
||||
|
||||
```text
|
||||
masjid/community
|
||||
ministry
|
||||
sunday
|
||||
bible
|
||||
servant
|
||||
service_day
|
||||
```
|
||||
|
||||
Those words may appear in `IslamicSundaySchool`, not `SchoolCore`.
|
||||
|
||||
## Contract Review Checklist
|
||||
|
||||
Before approving a contract, answer:
|
||||
|
||||
- Is the name globally meaningful for any school?
|
||||
- Does it require `SchoolContext`?
|
||||
- Does it avoid `Request`?
|
||||
- Does it avoid `auth()`?
|
||||
- Does it avoid Eloquent leakage across module boundaries?
|
||||
- Does it describe transaction ownership?
|
||||
- Does it describe authorization expectations?
|
||||
- Can Islamic Sunday School customize behavior without editing the contract?
|
||||
- Can another school type use this contract without pretending to be a Islamic Sunday School?
|
||||
|
||||
## Implementation Tickets
|
||||
|
||||
### CC-001: Create Domain Namespace Skeleton
|
||||
|
||||
**Objective:** Create the folder and namespace structure for `SchoolCore`, `IslamicSundaySchool`, and `Shared`.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Add directory structure.
|
||||
- Add placeholder README files for each major module.
|
||||
- Document dependency direction in `app/Domain/README.md`.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- Namespace structure exists.
|
||||
- Dependency rule is documented.
|
||||
- No production behavior changes.
|
||||
|
||||
### CC-002: Create Core Contract Interfaces
|
||||
|
||||
**Objective:** Add first-pass contracts for students, attendance, finance, files, communication, academics, enrollment, and reporting.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Add contracts under `App\Domain\SchoolCore\Contracts`.
|
||||
- Add minimal DTO namespaces.
|
||||
- Add docblocks for transaction and authorization expectations.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- Contracts compile.
|
||||
- Contracts use `SchoolContext`.
|
||||
- Contracts do not mention Islamic Sunday School concepts.
|
||||
|
||||
### CC-003: Add Service Providers and Bindings
|
||||
|
||||
**Objective:** Bind contracts to initial implementations.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `SchoolCoreServiceProvider`.
|
||||
- Create `IslamicSundaySchoolServiceProvider`.
|
||||
- Register providers.
|
||||
- Bind first implementations.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- Container resolves all initial contracts.
|
||||
- Islamic Sunday School extension policies can override policy/provider contracts.
|
||||
- Core service bindings remain neutral.
|
||||
|
||||
### CC-004: Add Architecture Boundary Tests
|
||||
|
||||
**Objective:** Prevent forbidden dependencies.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Add tests for `SchoolCore -> IslamicSundaySchool` imports.
|
||||
- Add tests preventing `Request` in domain services.
|
||||
- Add tests preventing `auth()` in domain services.
|
||||
- Add tests for forbidden vocabulary in `SchoolCore` contracts.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- Tests fail when forbidden dependencies are introduced.
|
||||
- Tests run in CI.
|
||||
|
||||
### CC-005: Migrate Student Identifier Generation Behind Contract
|
||||
|
||||
**Objective:** Fix student identifier race condition and introduce the first real core contract usage.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `StudentIdentifierGeneratorContract`.
|
||||
- Add default implementation.
|
||||
- Update student creation flow.
|
||||
- Add unique constraint.
|
||||
- Add concurrency test.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- No duplicate student identifiers under concurrent creation.
|
||||
- Identifier format can be replaced by binding.
|
||||
- Existing API response is preserved.
|
||||
|
||||
### CC-006: Migrate Payment File Access Behind Policy Contract
|
||||
|
||||
**Objective:** Use contract architecture to fix payment file authorization.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `FileAccessPolicyContract`.
|
||||
- Create payment-specific policy implementation.
|
||||
- Replace filename-only access with payment-ID-based access.
|
||||
- Add audit log.
|
||||
- Add authorization tests.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- Guessed filenames cannot access files.
|
||||
- Cross-school file access fails.
|
||||
- Authorized finance/admin access succeeds.
|
||||
|
||||
### CC-007: Add Attendance Policy Stub
|
||||
|
||||
**Objective:** Prepare attendance extraction by separating attendance rules from scanner implementation.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- Create `AttendancePolicyContract`.
|
||||
- Create `AcademicCalendarProviderContract`.
|
||||
- Add default and Islamic Sunday School implementations.
|
||||
- Bind policy/provider implementations.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
|
||||
- Islamic Sunday School attendance rules are outside `SchoolCore`.
|
||||
- Scanner extraction can use contracts in Phase 4.
|
||||
|
||||
## Phase 2 Rollout Order
|
||||
|
||||
Use this order:
|
||||
|
||||
1. Create namespace skeleton.
|
||||
2. Add core contracts.
|
||||
3. Add DTO/read model skeletons.
|
||||
4. Add providers and empty bindings.
|
||||
5. Add architecture tests.
|
||||
6. Migrate student identifier generation.
|
||||
7. Migrate payment file access.
|
||||
8. Add attendance policy stub.
|
||||
9. Update modular platform plan with completed contracts.
|
||||
|
||||
## Phase 2 Done Definition
|
||||
|
||||
Phase 2 is complete when:
|
||||
|
||||
- Core contracts exist and compile.
|
||||
- Initial service providers are registered.
|
||||
- `SchoolCore` cannot depend on `IslamicSundaySchool` without failing tests.
|
||||
- Domain services cannot use `Request` or `auth()` without failing tests.
|
||||
- Student identifier generation is contract-based and race-safe.
|
||||
- Payment file access uses policy-based authorization.
|
||||
- Attendance has policy/provider contracts ready for extraction.
|
||||
- Islamic Sunday School behavior is expressed through extension contracts, not hardcoded core logic.
|
||||
|
||||
## Risks
|
||||
|
||||
### Risk: Too many contracts too early
|
||||
|
||||
Mitigation: only create contracts that are needed by migration slices. Keep method count small.
|
||||
|
||||
### Risk: Interfaces mirror bad legacy services
|
||||
|
||||
Mitigation: design contracts around platform capabilities, not current class names.
|
||||
|
||||
### Risk: Islamic Sunday School rules leak into core
|
||||
|
||||
Mitigation: forbidden vocabulary tests and provider-based extension points.
|
||||
|
||||
### Risk: Controllers keep calling old services
|
||||
|
||||
Mitigation: each touched controller must move toward contract injection.
|
||||
|
||||
### Risk: DTO explosion
|
||||
|
||||
Mitigation: create DTOs for cross-module payloads only. Do not manufacture object bureaucracy for two strings and a prayer.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 2 does not fully refactor finance, attendance, students, communication, or reporting.
|
||||
|
||||
Phase 2 does not remove every legacy service.
|
||||
|
||||
Phase 2 does not redesign every database table.
|
||||
|
||||
Phase 2 creates the contract layer and proves the boundary works through a few real migration slices.
|
||||
|
||||
## Final Notes
|
||||
|
||||
This phase must be boring on purpose. Contracts are not where creativity belongs. They are where the platform decides what it promises and what it refuses to know.
|
||||
|
||||
The most important rule: `SchoolCore` must remain generic. Islamic Sunday School extends it. It does not secretly define it.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user