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,24 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\IslamicSundaySchool\Attendance;
use App\Domain\IslamicSundaySchool\Attendance\Policies\IslamicSundaySchoolAttendanceStatusPolicy;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
final class IslamicSundaySchoolAttendanceStatusPolicyTest extends TestCase
{
public function test_extension_can_use_different_late_window_without_changing_core(): void
{
$context = new SchoolContext(1, 10, [], '2026', 'spring', 'UTC', 'en', 'USD', 'islamic_sunday_school');
$session = new AttendanceSessionView(1, 1, 'Program Session', 'program', new DateTimeImmutable('2026-05-29 09:00:00'), new DateTimeImmutable('2026-05-29 12:00:00'), 'UTC');
$policy = new IslamicSundaySchoolAttendanceStatusPolicy();
$this->assertSame('present', $policy->statusForScan($context, $session, new DateTimeImmutable('2026-05-29 09:14:00'))->status);
$this->assertSame('late', $policy->statusForScan($context, $session, new DateTimeImmutable('2026-05-29 09:16:00'))->status);
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\IslamicSundaySchool;
use App\Domain\IslamicSundaySchool\Attendance\IslamicSundaySchoolAttendancePolicy;
use App\Domain\SchoolCore\Context\SchoolContext;
use PHPUnit\Framework\TestCase;
final class AttendancePolicyTest extends TestCase
{
public function test_it_treats_sunday_as_school_day(): void
{
$context = new SchoolContext(1, 2, [], '2025', 'spring', 'UTC', 'en', 'USD', 'islamic_sunday_school');
$policy = new IslamicSundaySchoolAttendancePolicy();
$this->assertTrue($policy->isSchoolDay($context, new \DateTimeImmutable('2026-05-31')));
$this->assertFalse($policy->isSchoolDay($context, new \DateTimeImmutable('2026-05-30')));
}
}
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace Tests\Unit\IslamicSundaySchool\Communication; use App\Domain\IslamicSundaySchool\Communication\Templates\IslamicSundaySchoolTemplateCatalog; use PHPUnit\Framework\TestCase; final class IslamicSundaySchoolCommunicationExtensionTest extends TestCase { public function test_template_catalog_is_extension_owned(): void { $this->assertArrayHasKey('program_class_reminder',(new IslamicSundaySchoolTemplateCatalog())->templates()); } }
@@ -0,0 +1,14 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use App\Domain\IslamicSundaySchool\Reporting\Reports\QuranProgressReport;
final class IslamicSundaySchoolReportAvailabilityTest extends TestCase
{
public function test_islamic_report_only_available_for_islamic_profile(): void
{
$report = new QuranProgressReport();
$this->assertTrue($report->availableForDomainProfile('islamic_sunday_school'));
$this->assertFalse($report->availableForDomainProfile('standard_school'));
}
}
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\IslamicSundaySchool\Students;
use App\Domain\IslamicSundaySchool\Students\Policies\IslamicSundaySchoolStudentIdentifierGenerator; use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\DTO\StudentView; use PHPUnit\Framework\TestCase;
final class IslamicSundaySchoolStudentIdentifierGeneratorTest extends TestCase { public function test_generates_extension_identifier_without_changing_core(): void { $ctx=new SchoolContext(1,9,[1],'2026','spring','America/New_York','en','USD','islamic_sunday_school'); $id=(new IslamicSundaySchoolStudentIdentifierGenerator())->generateForPersistedStudent($ctx,new StudentView(55,1,null,'A','B')); $this->assertStringStartsWith('SS-',(string)$id); $this->assertStringEndsWith('-000055',(string)$id); } }
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Attendance;
use App\Domain\SchoolCore\Attendance\DTO\AttendanceSessionView;
use App\Domain\SchoolCore\Attendance\Policies\DefaultAttendanceStatusPolicy;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
final class DefaultAttendanceStatusPolicyTest extends TestCase
{
public function test_status_becomes_late_after_default_threshold(): void
{
$context = new SchoolContext(1, 10, [], '2026', 'spring', 'UTC', 'en', 'USD', 'standard_school');
$session = new AttendanceSessionView(1, 1, 'Morning', 'daily', new DateTimeImmutable('2026-05-29 09:00:00'), new DateTimeImmutable('2026-05-29 12:00:00'), 'UTC');
$policy = new DefaultAttendanceStatusPolicy();
$this->assertSame('present', $policy->statusForScan($context, $session, new DateTimeImmutable('2026-05-29 09:05:00'))->status);
$this->assertSame('late', $policy->statusForScan($context, $session, new DateTimeImmutable('2026-05-29 09:11:00'))->status);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Attendance;
use App\Domain\SchoolCore\Attendance\DTO\ProcessScanData;
use App\Domain\SchoolCore\Attendance\Support\ScanFingerprint;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
final class ScanFingerprintTest extends TestCase
{
public function test_scan_fingerprint_is_school_scoped(): void
{
$scan = new ProcessScanData('ABC123', 'front', 'check_in', 'scanner', new DateTimeImmutable('2026-05-29 09:00:00'));
$a = new SchoolContext(1, 10, [], '2026', 'spring', 'UTC', 'en', 'USD', 'standard_school');
$b = new SchoolContext(2, 10, [], '2026', 'spring', 'UTC', 'en', 'USD', 'standard_school');
$this->assertNotSame(
ScanFingerprint::fromScan($a, $scan, 90),
ScanFingerprint::fromScan($b, $scan, 90)
);
}
}
@@ -0,0 +1 @@
<?php declare(strict_types=1); namespace Tests\Unit\SchoolCore\Communication; use App\Domain\SchoolCore\Communication\DTO\RecipientData; use App\Domain\SchoolCore\Communication\Services\RecipientDedupeService; use App\Domain\SchoolCore\Communication\Support\RecipientSet; use PHPUnit\Framework\TestCase; final class RecipientDedupeServiceTest extends TestCase { public function test_dedupes_same_contact(): void { $r=new RecipientData('guardian',1,'Guardian','same@example.test',null,null,[],null,null,10,['email']); $result=(new RecipientDedupeService())->dedupe(new RecipientSet([$r,$r])); $this->assertSame(1,$result->count()); $this->assertNotEmpty($result->excluded); } }
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Context;
use App\Domain\SchoolCore\Context\SchoolContext;
use PHPUnit\Framework\TestCase;
final class SchoolContextTest extends TestCase
{
public function test_it_builds_audit_payload(): void
{
$context = new SchoolContext(
schoolId: 12,
actorUserId: 44,
actorRoleIds: [1, 2],
schoolYear: '2025-2026',
term: 'spring',
timezone: 'America/New_York',
locale: 'en',
currency: 'USD',
domainProfile: SchoolContext::DOMAIN_ISLAMIC_SUNDAY_SCHOOL,
);
$this->assertTrue($context->isIslamicSundaySchool());
$this->assertSame(12, $context->auditPayload()['school_id']);
$this->assertSame('spring', $context->term);
$this->assertSame('fall', $context->withTerm('fall')->term);
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Context;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Context\SchoolContextException;
use App\Domain\SchoolCore\Context\SchoolContextValidator;
use Illuminate\Contracts\Auth\Authenticatable;
use Tests\TestCase;
final class SchoolContextValidatorTest extends TestCase
{
public function test_it_rejects_invalid_timezone(): void
{
$this->expectException(SchoolContextException::class);
$validator = new SchoolContextValidator();
$validator->assertValid($this->context(timezone: 'Not/AZone'), new FakeActor(10, 99));
}
public function test_it_rejects_wrong_school_actor(): void
{
$this->expectException(SchoolContextException::class);
$validator = new SchoolContextValidator();
$validator->assertValid($this->context(schoolId: 20), new FakeActor(10, 99));
}
private function context(int $schoolId = 10, string $timezone = 'UTC'): SchoolContext
{
return new SchoolContext(
schoolId: $schoolId,
actorUserId: 99,
actorRoleIds: [],
schoolYear: '2025-2026',
term: 'spring',
timezone: $timezone,
locale: 'en',
currency: 'USD',
domainProfile: SchoolContext::DOMAIN_STANDARD_SCHOOL,
);
}
}
final class FakeActor 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,20 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Finance;
use App\Domain\SchoolCore\Finance\Services\BalanceCalculator;
use PHPUnit\Framework\TestCase;
final class BalanceCalculatorTest extends TestCase
{
public function test_payment_reduces_balance(): void
{
$result = (new BalanceCalculator())->calculate(['currency'=>'USD'], [['amount_minor'=>10000,'type'=>'charge']], [['amount_minor'=>4000,'status'=>'recorded']]);
$this->assertSame(10000, $result['total_amount_minor']);
$this->assertSame(4000, $result['paid_amount_minor']);
$this->assertSame(6000, $result['balance_amount_minor']);
$this->assertSame('partial', $result['status']);
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Finance;
use App\Domain\SchoolCore\Finance\Support\Money;
use PHPUnit\Framework\TestCase;
final class MoneyTest extends TestCase
{
public function test_money_adds_and_compares_without_float_math(): void
{
$a = new Money(12500, 'USD'); $b = new Money(2500, 'USD');
$this->assertSame(15000, $a->add($b)->minorAmount());
$this->assertSame(10000, $a->subtract($b)->minorAmount());
$this->assertSame(1, $a->compare($b));
$this->assertSame('125.00', $a->toDecimalString());
}
}
@@ -0,0 +1,14 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use App\Domain\SchoolCore\Reporting\Services\ReportRegistry;
use App\Domain\SchoolCore\Reporting\Reports\Students\StudentRosterReport;
final class ReportRegistryTest extends TestCase
{
public function test_registers_and_returns_report_by_key(): void
{
$registry = new ReportRegistry([new StudentRosterReport()]);
$this->assertSame('students.roster', $registry->get('students.roster')->key());
}
}
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Students;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\DTO\StudentView; use App\Domain\SchoolCore\Students\Policies\DefaultStudentIdentifierGenerator; use PHPUnit\Framework\TestCase;
final class DefaultStudentIdentifierGeneratorTest extends TestCase { public function test_generates_identifier_from_persisted_student_id(): void { $ctx=new SchoolContext(1,9,[1],'2026','spring','America/New_York','en','USD','standard_school'); $id=(new DefaultStudentIdentifierGenerator())->generateForPersistedStudent($ctx,new StudentView(123,1,null,'A','B')); $this->assertSame('SCH-0000123',(string)$id); } }
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Students;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\Services\SequentialStudentIdentifierGenerator;
use PHPUnit\Framework\TestCase;
final class SequentialStudentIdentifierGeneratorTest extends TestCase
{
public function test_it_generates_identifier_after_student_id_exists(): void
{
$context = new SchoolContext(12, 44, [], '2025', 'spring', 'UTC', 'en', 'USD', 'standard_school');
$identifier = (new SequentialStudentIdentifierGenerator())->generate($context, 991);
$this->assertSame('12-000991', $identifier);
}
}
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\SchoolCore\Students;
use App\Domain\SchoolCore\Students\Exceptions\InvalidStudentStatusTransitionException; use App\Domain\SchoolCore\Students\Support\StudentLifecycleStateMachine; use PHPUnit\Framework\TestCase;
final class StudentLifecycleStateMachineTest extends TestCase { public function test_allows_valid_transition(): void { (new StudentLifecycleStateMachine())->assertCanTransition('active','withdrawn'); $this->assertTrue(true); } public function test_rejects_invalid_transition(): void { $this->expectException(InvalidStudentStatusTransitionException::class); (new StudentLifecycleStateMachine())->assertCanTransition('archived','active'); } }