update project

This commit is contained in:
root
2026-05-30 01:11:35 -04:00
parent 3a0628ecc7
commit 2225f6bc72
9743 changed files with 1122482 additions and 59 deletions
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace Tests\Feature\Api;
use App\Support\Api\ApiResponse;
use PHPUnit\Framework\TestCase;
final class Phase8ApiEnvelopeTest extends TestCase
{
public function test_api_response_class_exists(): void
{
$this->assertTrue(class_exists(ApiResponse::class));
}
public function test_route_inventory_fixture_exists(): void
{
$path = dirname(__DIR__, 3).'/storage/app/generated/api-route-inventory.json';
$this->assertFileExists($path);
$this->assertStringContainsString('api.v2', file_get_contents($path));
}
}
@@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Integration;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
final class AuthTenantIsolationIntegrationTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_access_own_school_workflows(): void
{
$school = $this->createSchool('Own School', 'OWN');
$user = $this->createActorForSchool($school);
$this->actingAs($user);
$this->getJson("/api/schools/{$school}")
->assertOk();
$studentResponse = $this->postJson("/api/schools/{$school}/students", [
'first_name' => 'Maya',
'last_name' => 'Patel',
'student_number' => 'OWN-1001',
'grade_level' => '7',
]);
$studentResponse->assertCreated();
$studentId = $studentResponse->json('data.id');
$this->getJson("/api/schools/{$school}/students/{$studentId}")
->assertOk()
->assertJsonPath('data.student_number', 'OWN-1001');
$this->postJson("/api/schools/{$school}/attendance/scans", [
'student_number' => 'OWN-1001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])->assertCreated();
$this->createTemplate(
$school,
'attendance_absence',
'email',
'Hello, {{ student.first_name }} was marked absent.'
);
$this->postJson("/api/schools/{$school}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $studentId,
'channel' => 'email',
])->assertOk();
$this->postJson("/api/schools/{$school}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $studentId,
'channel' => 'email',
])->assertAccepted();
$file = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nOWN-1001,125.00,2026-05-29,PAY-OWN-001\n"
);
$this->postJson("/api/schools/{$school}/finance/payment-files", [
'file' => $file,
])->assertCreated();
$this->getJson("/api/schools/{$school}/reports/daily?date=2026-05-29")
->assertOk();
}
public function test_user_cannot_access_another_schools_data(): void
{
$schoolA = $this->createSchool('School A', 'S-A');
$schoolB = $this->createSchool('School B', 'S-B');
$userA = $this->createActorForSchool($schoolA);
$studentB = $this->createStudent($schoolB, 'OTHER-2001', 'Other');
$this->createTemplate(
$schoolB,
'attendance_absence',
'email',
'Hello, {{ student.first_name }} was marked absent.'
);
$this->actingAs($userA);
$this->assertTenantBlocked(
$this->getJson("/api/schools/{$schoolB}")
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/students", [
'first_name' => 'Intruder',
'last_name' => 'Student',
'student_number' => 'BAD-1001',
'grade_level' => '8',
])
);
$this->assertTenantBlocked(
$this->getJson("/api/schools/{$schoolB}/students/{$studentB}")
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/attendance/scans", [
'student_number' => 'OTHER-2001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $studentB,
'channel' => 'email',
])
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $studentB,
'channel' => 'email',
])
);
$file = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nOTHER-2001,125.00,2026-05-29,PAY-BAD-001\n"
);
$this->assertTenantBlocked(
$this->postJson("/api/schools/{$schoolB}/finance/payment-files", [
'file' => $file,
])
);
$this->assertTenantBlocked(
$this->getJson("/api/schools/{$schoolB}/reports/daily?date=2026-05-29")
);
$this->assertDatabaseMissing('students', [
'school_id' => $schoolB,
'student_identifier' => 'BAD-1001',
]);
$this->assertDatabaseMissing('payments', [
'school_id' => $schoolB,
'reference_number' => 'PAY-BAD-001',
]);
}
public function test_guest_cannot_access_school_workflows(): void
{
$school = $this->createSchool('Guest Block School', 'GST');
$student = $this->createStudent($school, 'GUEST-1001', 'Guest');
$this->createTemplate(
$school,
'attendance_absence',
'email',
'Hello, {{ student.first_name }} was marked absent.'
);
$this->getJson("/api/schools/{$school}")
->assertUnauthorized();
$this->postJson("/api/schools/{$school}/students", [
'first_name' => 'Guest',
'last_name' => 'Student',
'student_number' => 'GUEST-2001',
'grade_level' => '7',
])->assertUnauthorized();
$this->getJson("/api/schools/{$school}/students/{$student}")
->assertUnauthorized();
$this->postJson("/api/schools/{$school}/attendance/scans", [
'student_number' => 'GUEST-1001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])->assertUnauthorized();
$this->postJson("/api/schools/{$school}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $student,
'channel' => 'email',
])->assertUnauthorized();
$this->postJson("/api/schools/{$school}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $student,
'channel' => 'email',
])->assertUnauthorized();
$file = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nGUEST-1001,125.00,2026-05-29,PAY-GUEST-001\n"
);
$this->postJson("/api/schools/{$school}/finance/payment-files", [
'file' => $file,
])->assertUnauthorized();
$this->getJson("/api/schools/{$school}/reports/daily?date=2026-05-29")
->assertUnauthorized();
}
private function createSchool(string $name = 'Test School', ?string $code = null): int
{
return (int) DB::table('schools')->insertGetId([
'name' => $name,
'code' => $code ?? 'SCH-'.uniqid(),
'domain_profile' => 'standard_school',
'timezone' => 'UTC',
'locale' => 'en',
'currency' => 'USD',
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createStudent(int $school, string $studentNumber, string $firstName = 'Maya'): int
{
return (int) DB::table('students')->insertGetId([
'school_id' => $school,
'school_id_number' => $studentNumber,
'student_identifier' => $studentNumber,
'first_name' => $firstName,
'last_name' => 'Patel',
'status' => 'active',
'profile_metadata' => json_encode(['grade_level' => '7'], JSON_THROW_ON_ERROR),
'metadata' => json_encode(['grade_level' => '7'], JSON_THROW_ON_ERROR),
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createTemplate(int $school, string $key, string $channel, string $body): int
{
return (int) DB::table('communication_templates')->insertGetId([
'school_id' => $school,
'template_key' => $key,
'version' => '1',
'status' => 'active',
'channel' => $channel,
'message_category' => 'general',
'language' => 'en',
'subject' => 'Notice for {{ student.first_name }}',
'body' => $body,
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createActorForSchool(int $school): Authenticatable
{
$this->ensureAuthTestTablesExist();
$userId = (int) DB::table('users')->insertGetId([
'name' => 'Test User '.$school,
'email' => 'user'.$school.'@example.test',
'password' => bcrypt('password'),
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('school_user')->insert([
'school_id' => $school,
'user_id' => $userId,
'role' => 'admin',
'created_at' => now(),
'updated_at' => now(),
]);
return new GenericUser([
'id' => $userId,
'name' => 'Test User '.$school,
'email' => 'user'.$school.'@example.test',
]);
}
private function ensureAuthTestTablesExist(): void
{
if (! Schema::hasTable('users')) {
Schema::create('users', function ($table): void {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
if (! Schema::hasTable('school_user')) {
Schema::create('school_user', function ($table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('user_id')->index();
$table->string('role')->default('admin');
$table->timestamps();
$table->unique(['school_id', 'user_id']);
});
}
}
private function assertTenantBlocked($response): void
{
$this->assertContains(
$response->getStatusCode(),
[403, 404],
'Cross-tenant access must return either 403 or 404.'
);
}
}
@@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Integration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
final class SchoolWorkflowIntegrationTest extends TestCase
{
use RefreshDatabase;
public function test_school_can_be_created_and_read_through_http(): void
{
$response = $this->postJson('/api/schools', [
'name' => 'North Ridge School',
'code' => 'NRS',
'timezone' => 'America/New_York',
]);
$response->assertCreated()
->assertJsonPath('data.name', 'North Ridge School')
->assertJsonPath('data.code', 'NRS');
$schoolId = (int) $response->json('data.id');
$this->actingAsSchoolUser($schoolId);
$this->getJson("/api/schools/{$schoolId}")
->assertOk()
->assertJsonPath('data.timezone', 'America/New_York');
$this->assertDatabaseHas('schools', [
'id' => $schoolId,
'code' => 'NRS',
]);
}
public function test_student_can_be_created_for_school_and_isolated_by_school(): void
{
$schoolA = $this->createSchool('School A', 'S-A');
$schoolB = $this->createSchool('School B', 'S-B');
$response = $this->postJson("/api/schools/{$schoolA}/students", [
'first_name' => 'Maya',
'last_name' => 'Patel',
'student_number' => 'STU-1001',
'grade_level' => '7',
]);
$response->assertCreated()
->assertJsonPath('data.first_name', 'Maya')
->assertJsonPath('data.student_number', 'STU-1001');
$studentId = $response->json('data.id');
$this->getJson("/api/schools/{$schoolA}/students/{$studentId}")
->assertOk()
->assertJsonPath('data.grade_level', '7');
$this->getJson("/api/schools/{$schoolB}/students/{$studentId}")
->assertNotFound();
}
public function test_attendance_scan_records_once_and_duplicate_scan_is_idempotent(): void
{
$school = $this->createSchool();
$this->actingAsSchoolUser($school);
$student = $this->createStudent($school, 'STU-2001');
$payload = [
'student_number' => 'STU-2001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
];
$this->postJson("/api/schools/{$school}/attendance/scans", $payload)
->assertCreated()
->assertJsonPath('data.status', 'recorded');
$this->postJson("/api/schools/{$school}/attendance/scans", $payload)
->assertOk()
->assertJsonPath('data.status', 'duplicate');
$this->assertDatabaseHas('attendance_records', [
'school_id' => $school,
'subject_type' => 'student',
'subject_id' => $student,
'status' => 'present',
]);
$this->assertSame(1, DB::table('attendance_records')->where('school_id', $school)->where('subject_id', $student)->count());
$this->assertSame(2, DB::table('attendance_scan_logs')->where('school_id', $school)->where('subject_id', $student)->count());
}
public function test_communication_preview_has_no_side_effect_and_send_queues_recipient(): void
{
$school = $this->createSchool();
$this->actingAsSchoolUser($school);
$student = $this->createStudent($school, 'STU-3001', 'Maya');
$this->createTemplate($school, 'attendance_absence', 'email', 'Hello, {{ student.first_name }} was marked absent.');
$this->postJson("/api/schools/{$school}/communications/preview", [
'template_key' => 'attendance_absence',
'student_id' => $student,
'channel' => 'email',
])->assertOk()
->assertJsonPath('data.body', 'Hello, Maya was marked absent.');
$this->assertDatabaseCount('communication_messages', 0);
$this->assertDatabaseCount('communication_message_recipients', 0);
$this->postJson("/api/schools/{$school}/communications/send", [
'template_key' => 'attendance_absence',
'student_id' => $student,
'channel' => 'email',
])->assertAccepted()
->assertJsonPath('data.status', 'queued');
$this->assertDatabaseHas('communication_messages', [
'school_id' => $school,
'channel' => 'email',
'status' => 'queued',
]);
$this->assertDatabaseHas('communication_message_recipients', [
'school_id' => $school,
'recipient_type' => 'student',
'recipient_id' => $student,
'status' => 'queued',
]);
}
public function test_payment_file_imports_valid_rows_and_rejects_invalid_rows_without_partial_import(): void
{
$school = $this->createSchool();
$this->actingAsSchoolUser($school);
$student = $this->createStudent($school, 'STU-4001');
$validFile = UploadedFile::fake()->createWithContent(
'payments.csv',
"student_number,amount,paid_at,reference\nSTU-4001,125.00,2026-05-29,PAY-001\n"
);
$this->postJson("/api/schools/{$school}/finance/payment-files", [
'file' => $validFile,
])->assertCreated()
->assertJsonPath('data.imported_count', 1);
$this->assertDatabaseHas('payments', [
'school_id' => $school,
'student_id' => $student,
'amount_minor' => 12500,
'reference_number' => 'PAY-001',
]);
$invalidFile = UploadedFile::fake()->createWithContent(
'bad-payments.csv',
"student_number,amount,paid_at,reference\nSTU-4001,80.00,2026-05-29,PAY-002\nMISSING,90.00,2026-05-29,PAY-003\n"
);
$this->postJson("/api/schools/{$school}/finance/payment-files", [
'file' => $invalidFile,
])->assertUnprocessable();
$this->assertDatabaseMissing('payments', [
'school_id' => $school,
'reference_number' => 'PAY-002',
]);
$this->assertDatabaseMissing('payments', [
'school_id' => $school,
'reference_number' => 'PAY-003',
]);
}
public function test_daily_report_aggregates_attendance_and_finance(): void
{
$school = $this->createSchool();
$this->actingAsSchoolUser($school);
$student = $this->createStudent($school, 'STU-5001');
$this->postJson("/api/schools/{$school}/attendance/scans", [
'student_number' => 'STU-5001',
'scanned_at' => '2026-05-29T08:15:00Z',
'source' => 'kiosk',
])->assertCreated();
DB::table('payments')->insert([
'school_id' => $school,
'student_id' => $student,
'amount_minor' => 12500,
'currency' => 'USD',
'payment_method' => 'manual',
'payment_date' => '2026-05-29',
'reference_number' => 'PAY-RPT-001',
'status' => 'recorded',
'created_at' => now(),
'updated_at' => now(),
]);
$this->getJson("/api/schools/{$school}/reports/daily?date=2026-05-29")
->assertOk()
->assertJsonPath('data.attendance.present_count', 1)
->assertJsonPath('data.finance.total_payments', 12500);
}
private function createSchool(string $name = 'Test School', ?string $code = null): int
{
return (int) DB::table('schools')->insertGetId([
'name' => $name,
'code' => $code ?? 'SCH-'.uniqid(),
'domain_profile' => 'standard_school',
'timezone' => 'UTC',
'locale' => 'en',
'currency' => 'USD',
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createStudent(int $school, string $studentNumber, string $firstName = 'Maya'): int
{
return (int) DB::table('students')->insertGetId([
'school_id' => $school,
'school_id_number' => $studentNumber,
'student_identifier' => $studentNumber,
'first_name' => $firstName,
'last_name' => 'Patel',
'status' => 'active',
'profile_metadata' => json_encode(['grade_level' => '7'], JSON_THROW_ON_ERROR),
'metadata' => json_encode(['grade_level' => '7'], JSON_THROW_ON_ERROR),
'created_at' => now(),
'updated_at' => now(),
]);
}
private function createTemplate(int $school, string $key, string $channel, string $body): int
{
return (int) DB::table('communication_templates')->insertGetId([
'school_id' => $school,
'template_key' => $key,
'version' => '1',
'status' => 'active',
'channel' => $channel,
'message_category' => 'general',
'language' => 'en',
'subject' => 'Notice for {{ student.first_name }}',
'body' => $body,
'created_at' => now(),
'updated_at' => now(),
]);
}
private function actingAsSchoolUser(int $school): Authenticatable
{
$this->ensureAuthTestTablesExist();
$userId = (int) DB::table('users')->insertGetId([
'name' => 'Workflow User '.$school,
'email' => 'workflow'.$school.'@example.test',
'password' => bcrypt('password'),
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('school_user')->insert([
'school_id' => $school,
'user_id' => $userId,
'role' => 'admin',
'created_at' => now(),
'updated_at' => now(),
]);
$user = new GenericUser([
'id' => $userId,
'name' => 'Workflow User '.$school,
'email' => 'workflow'.$school.'@example.test',
]);
$this->actingAs($user);
return $user;
}
private function ensureAuthTestTablesExist(): void
{
if (! Schema::hasTable('users')) {
Schema::create('users', function ($table): void {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
if (! Schema::hasTable('school_user')) {
Schema::create('school_user', function ($table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('user_id')->index();
$table->string('role')->default('admin');
$table->timestamps();
$table->unique(['school_id', 'user_id']);
});
}
}
}
@@ -0,0 +1,22 @@
<?php
namespace Tests\Feature\Release;
use App\Support\Release\FeatureFlagRegistry;
use PHPUnit\Framework\TestCase;
final class FeatureFlagRolloutMapTest extends TestCase
{
public function test_high_risk_flags_have_owners_and_safe_defaults(): void
{
$registry = new FeatureFlagRegistry();
$flags = $registry->export();
$this->assertNotEmpty($flags);
foreach ($flags as $flag) {
$this->assertNotSame('', $flag['owner']);
$this->assertArrayHasKey('default', $flag);
}
}
}
@@ -0,0 +1,22 @@
<?php
namespace Tests\Feature\Release;
use App\Support\Release\MigrationValidationRunner;
use PHPUnit\Framework\TestCase;
final class MigrationValidationScaffoldTest extends TestCase
{
public function test_validation_runner_defines_required_module_checks(): void
{
$runner = new MigrationValidationRunner();
$modules = array_map(fn ($result): string => $result->module, $runner->runAll());
$this->assertContains('finance', $modules);
$this->assertContains('attendance', $modules);
$this->assertContains('students', $modules);
$this->assertContains('communication', $modules);
$this->assertContains('reporting', $modules);
$this->assertContains('islamic_sunday_school', $modules);
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\SchoolCore\Attendance;
use PHPUnit\Framework\TestCase;
final class ScannerDelegationTest extends TestCase
{
public function test_scanner_route_file_exists_for_phase_four_slice(): void
{
$routeFile = dirname(__DIR__, 4).'/routes/attendance_phase4.php';
$this->assertFileExists($routeFile);
$this->assertStringContainsString('attendance.scan.process', file_get_contents($routeFile));
}
}
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace Tests\Feature\SchoolCore\Communication; use App\Domain\SchoolCore\Communication\Exceptions\RecipientPreviewRequiredException; use PHPUnit\Framework\TestCase; final class BulkSendRequiresPreviewTest extends TestCase { public function test_bulk_send_without_preview_exception_exists(): void { $this->assertTrue(class_exists(RecipientPreviewRequiredException::class)); } }
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\SchoolCore\Finance;
use PHPUnit\Framework\TestCase;
final class PaymentFileRouteTest extends TestCase
{
public function test_phase3_route_documents_payment_id_based_file_access(): void
{
$routeFile = dirname(__DIR__, 4).'/routes/finance_phase3.php';
$this->assertFileExists($routeFile);
$routes = file_get_contents($routeFile);
$this->assertStringContainsString('payments/{payment}/file', $routes);
$this->assertStringContainsString('school.context', $routes);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\SchoolCore;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Files\Policies\PaymentFileAccessPolicy;
use PHPUnit\Framework\TestCase;
final class PaymentFilePolicyTest extends TestCase
{
public function test_cross_school_payment_file_access_fails(): void
{
$context = new SchoolContext(10, 20, ['finance'], '2025', 'spring', 'UTC', 'en', 'USD', 'standard_school');
$policy = new PaymentFileAccessPolicy();
$this->assertFalse($policy->canAccess($context, 'payment', 99, ['school_id' => 11]));
}
public function test_finance_role_can_access_same_school_payment_file(): void
{
$context = new SchoolContext(10, 20, ['finance'], '2025', 'spring', 'UTC', 'en', 'USD', 'standard_school');
$policy = new PaymentFileAccessPolicy();
$this->assertTrue($policy->canAccess($context, 'payment', 99, ['school_id' => 10]));
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ReportingPhase7ScaffoldTest extends TestCase
{
public function test_reporting_phase_7_scaffold_exists(): void
{
$root = dirname(__DIR__, 4);
$this->assertFileExists($root.'/app/Domain/SchoolCore/Reporting/Contracts/ReportRunnerContract.php');
$this->assertFileExists($root.'/app/Providers/SchoolCoreReportingServiceProvider.php');
$this->assertFileExists($root.'/app/Domain/IslamicSundaySchool/Reporting/Providers/IslamicSundaySchoolReportingServiceProvider.php');
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\SchoolCore;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Http\Middleware\ResolveSchoolContext;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Orchestra\Testbench\TestCase;
final class ResolveSchoolContextMiddlewareTest extends TestCase
{
protected function getPackageProviders($app): array
{
return [\App\Providers\SchoolContextServiceProvider::class];
}
public function test_it_attaches_context_to_request(): void
{
$request = Request::create('/test', 'GET');
$request->setUserResolver(fn () => new MiddlewareFakeUser(5, 88));
$middleware = app(ResolveSchoolContext::class);
$response = $middleware->handle($request, function (Request $request) {
return response()->json([
'school_id' => $request->attributes->get(SchoolContext::class)->schoolId,
]);
});
$this->assertSame(200, $response->getStatusCode());
$this->assertStringContainsString('5', $response->getContent());
}
}
final class MiddlewareFakeUser implements Authenticatable
{
public function __construct(public int $school_id, private int $id)
{
}
public function getAuthIdentifierName(): string { return 'id'; }
public function getAuthIdentifier(): mixed { return $this->id; }
public function getAuthPassword(): string { return ''; }
public function getAuthPasswordName(): string { return 'password'; }
public function getRememberToken(): ?string { return null; }
public function setRememberToken($value): void {}
public function getRememberTokenName(): string { return 'remember_token'; }
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\SchoolCore;
use PHPUnit\Framework\TestCase;
final class StudentIdentifierConcurrencyTest extends TestCase
{
public function test_database_must_enforce_unique_student_identifier_per_school(): void
{
$this->markTestSkipped('Enable in the host Laravel app after binding the real Student model and database connection. The migration adds unique(school_id, school_id_number).');
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\SchoolCore\Students;
use PHPUnit\Framework\TestCase;
final class StudentLifecycleDelegationTest extends TestCase
{
public function test_phase5_routes_file_exists(): void
{
$routeFile = dirname(__DIR__, 4).'/routes/student_lifecycle_phase5.php';
$this->assertFileExists($routeFile);
}
}