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,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\AssignmentData;
use App\Domain\SchoolCore\Students\DTO\AssignmentResult;
interface AssignmentServiceContract
{
public function assignStudent(SchoolContext $context, AssignmentData $data): AssignmentResult;
public function unassignStudent(SchoolContext $context, int $assignmentId, string $reason): AssignmentResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\EnrollmentData;
use App\Domain\SchoolCore\Students\DTO\EnrollmentResult;
interface EnrollmentServiceContract
{
public function enrollStudent(SchoolContext $context, EnrollmentData $data): EnrollmentResult;
public function withdrawStudent(SchoolContext $context, int $studentId, string $reason): EnrollmentResult;
public function transferStudent(SchoolContext $context, int $studentId, EnrollmentData $data): EnrollmentResult;
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\CreateGuardianData;
use App\Domain\SchoolCore\Students\DTO\GuardianLinkResult;
use App\Domain\SchoolCore\Students\DTO\GuardianResult;
use App\Domain\SchoolCore\Students\DTO\LinkGuardianData;
interface GuardianServiceContract
{
public function createGuardian(SchoolContext $context, CreateGuardianData $data): GuardianResult;
public function linkGuardianToStudent(SchoolContext $context, LinkGuardianData $data): GuardianLinkResult;
public function unlinkGuardianFromStudent(SchoolContext $context, int $studentId, int $guardianId, string $reason): GuardianLinkResult;
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\HouseholdData;
use App\Domain\SchoolCore\Students\DTO\HouseholdResult;
interface HouseholdServiceContract
{
public function createHousehold(SchoolContext $context, HouseholdData $data): HouseholdResult;
public function addStudentToHousehold(SchoolContext $context, int $householdId, int $studentId): HouseholdResult;
public function addGuardianToHousehold(SchoolContext $context, int $householdId, int $guardianId): HouseholdResult;
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\EnrollmentView;
use App\Domain\SchoolCore\Students\DTO\PromotionEvaluationResult;
use App\Domain\SchoolCore\Students\DTO\StudentView;
interface PromotionPolicyContract
{
public function evaluate(SchoolContext $context, StudentView $student, EnrollmentView $enrollment): PromotionEvaluationResult;
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\BulkPromotionData;
use App\Domain\SchoolCore\Students\DTO\BulkPromotionResult;
use App\Domain\SchoolCore\Students\DTO\PromotionData;
use App\Domain\SchoolCore\Students\DTO\PromotionEvaluationResult;
use App\Domain\SchoolCore\Students\DTO\PromotionResultData;
interface PromotionServiceContract
{
public function evaluatePromotion(SchoolContext $context, int $studentId): PromotionEvaluationResult;
public function promoteStudent(SchoolContext $context, PromotionData $data): PromotionResultData;
public function bulkPromote(SchoolContext $context, BulkPromotionData $data): BulkPromotionResult;
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\StudentIdentifier;
use App\Domain\SchoolCore\Students\DTO\StudentView;
interface StudentIdentifierGeneratorContract
{
public function generateForPersistedStudent(SchoolContext $context, StudentView $student): StudentIdentifier;
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface StudentLifecycleAuditLoggerContract
{
public function log(SchoolContext $context, string $action, string $entityType, int $entityId, ?array $before = null, ?array $after = null, ?string $reason = null, ?string $idempotencyKey = null): void;
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\StudentProfileData;
use App\Domain\SchoolCore\Students\DTO\StudentResult;
interface StudentProfileServiceContract
{
public function updateProfile(SchoolContext $context, StudentProfileData $data): StudentResult;
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\StudentSearchQuery;
use App\Domain\SchoolCore\Students\DTO\StudentView;
interface StudentReadServiceContract
{
public function find(SchoolContext $context, int $studentId): ?StudentView;
public function search(SchoolContext $context, StudentSearchQuery $query): array;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\CreateStudentData;
use App\Domain\SchoolCore\Students\DTO\StudentResult;
use App\Domain\SchoolCore\Students\DTO\StudentStatusTransitionData;
use App\Domain\SchoolCore\Students\DTO\StudentView;
use App\Domain\SchoolCore\Students\DTO\UpdateStudentData;
interface StudentServiceContract
{
public function createStudent(SchoolContext $context, CreateStudentData $data): StudentResult;
public function updateStudent(SchoolContext $context, int $studentId, UpdateStudentData $data): StudentResult;
public function transitionStatus(SchoolContext $context, int $studentId, StudentStatusTransitionData $data): StudentResult;
public function getStudent(SchoolContext $context, int $studentId): StudentView;
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class AssignmentData
{
public function __construct(public int $studentId, public string $assignmentType, public string $assignableType, public int $assignableId, public string $effectiveFrom, public ?string $effectiveTo=null, public string $status='active', public array $metadata=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class AssignmentResult
{
public function __construct(public int $assignmentId, public int $studentId, public string $status='active', public array $attributes=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class BulkPromotionData
{
public function __construct(public array $studentIds, public int $fromAcademicYearId, public int $toAcademicYearId, public string $effectiveDate, public bool $overridePolicy=false, public ?string $overrideReason=null) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class BulkPromotionResult
{
public function __construct(public array $rows, public int $successCount=0, public int $failureCount=0) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class CreateGuardianData
{
public function __construct(public string $firstName, public string $lastName, public ?string $email=null, public ?string $phone=null, public ?string $relationshipType=null, public array $address=[], public bool $emergencyContact=false, public bool $pickupAuthorized=false, public array $metadata=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class CreateStudentData
{
public function __construct(public string $firstName, public string $lastName, public ?string $middleName=null, public ?string $preferredName=null, public ?string $dateOfBirth=null, public ?string $gender=null, public ?string $primaryLanguage=null, public mixed $photoFile=null, public ?string $externalId=null, public string $initialStatus='active', public array $guardians=[], public ?int $householdId=null, public ?EnrollmentData $enrollment=null, public array $profileMetadata=[], public array $extensionProfile=[], public ?string $idempotencyKey=null) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class EnrollmentData
{
public function __construct(public int $studentId, public int $academicYearId, public ?int $termId=null, public ?int $gradeLevelId=null, public ?int $programId=null, public ?int $classGroupId=null, public string $enrollmentDate='', public string $status='active', public ?string $reason=null, public array $metadata=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class EnrollmentResult
{
public function __construct(public EnrollmentView $enrollment, public array $messages=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class EnrollmentView
{
public function __construct(public int $id, public int $studentId, public int $schoolId, public string $status='active', public array $attributes=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class GuardianLinkResult
{
public function __construct(public int $studentId, public int $guardianId, public string $relationshipType, public bool $active=true) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class GuardianResult
{
public function __construct(public int $guardianId, public array $attributes=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class HouseholdData
{
public function __construct(public string $name, public ?string $primaryAddress=null, public array $metadata=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class HouseholdResult
{
public function __construct(public int $householdId, public array $attributes=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class LinkGuardianData
{
public function __construct(public int $studentId, public int $guardianId, public string $relationshipType, public bool $emergencyContact=false, public bool $pickupAuthorized=false, public ?bool $financialResponsibility=null, public ?bool $academicContact=null, public ?string $notes=null) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class PromotionData
{
public function __construct(public int $studentId, public int $fromAcademicYearId, public int $toAcademicYearId, public ?int $fromLevelId=null, public ?int $toLevelId=null, public ?int $fromGroupId=null, public ?int $toGroupId=null, public string $effectiveDate='', public string $reason='promotion', public bool $overridePolicy=false, public ?string $overrideReason=null) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class PromotionEvaluationResult
{
public function __construct(public bool $eligible, public string $result, public array $reasons=[], public array $recommendedPlacement=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class PromotionResultData
{
public function __construct(public int $studentId, public string $result, public array $attributes=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class StudentIdentifier
{
public function __construct(public string $value) { if ($value === '') { throw new \InvalidArgumentException('Student identifier cannot be empty.'); } } public function __toString(): string { return $this->value; }
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class StudentProfileData
{
public function __construct(public int $studentId, public array $profileMetadata=[], public array $extensionProfile=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class StudentResult
{
public function __construct(public StudentView $student, public array $messages=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class StudentSearchQuery
{
public function __construct(public ?string $search=null, public ?string $status='active', public ?int $guardianId=null, public ?int $householdId=null, public int $limit=50, public array $filters=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class StudentStatusTransitionData
{
public function __construct(public string $fromStatus, public string $toStatus, public string $reason, public array $metadata=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class StudentView
{
public function __construct(public int $id, public int $schoolId, public ?string $studentIdentifier, public string $firstName, public string $lastName, public string $status='active', public array $attributes=[]) {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class UpdateGuardianData
{
public function __construct(public ?string $firstName=null, public ?string $lastName=null, public ?string $email=null, public ?string $phone=null, public array $metadata=[], public string $reason='guardian update') {}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\DTO;
final readonly class UpdateStudentData
{
public function __construct(public ?string $firstName=null, public ?string $lastName=null, public ?string $middleName=null, public ?string $preferredName=null, public ?string $dateOfBirth=null, public ?string $gender=null, public ?string $primaryLanguage=null, public mixed $photoFile=null, public array $profileMetadata=[], public string $updateReason='student update') {}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Data;
final readonly class CreateStudentData
{
/** @param array<string, mixed> $attributes */
public function __construct(public array $attributes) {}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Data;
final readonly class StudentReadModel
{
/** @param array<string, mixed> $attributes */
public function __construct(public int $id, public ?string $identifier, public array $attributes = []) {}
public static function fromArray(array $student): self
{
return new self((int) $student['id'], $student['school_id_number'] ?? null, $student);
}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Data;
final readonly class UpdateStudentData
{
/** @param array<string, mixed> $attributes */
public function __construct(public array $attributes) {}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Enums;
enum AssignmentStatus: string
{
case ACTIVE = 'active';
case ENDED = 'ended';
case PENDING = 'pending';
case CANCELLED = 'cancelled';
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Enums;
enum EnrollmentStatus: string
{
case PENDING = 'pending';
case ACTIVE = 'active';
case WITHDRAWN = 'withdrawn';
case TRANSFERRED = 'transferred';
case COMPLETED = 'completed';
case INACTIVE = 'inactive';
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Enums;
enum GuardianRelationshipType: string
{
case PARENT = 'parent';
case GUARDIAN = 'guardian';
case RELATIVE = 'relative';
case EMERGENCY_CONTACT = 'emergency_contact';
case OTHER = 'other';
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Enums;
enum PromotionResult: string
{
case PROMOTED = 'promoted';
case RETAINED = 'retained';
case GRADUATED = 'graduated';
case MANUAL_REVIEW = 'manual_review';
case INELIGIBLE = 'ineligible';
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Enums;
enum StudentAuditAction: string
{
case STUDENT_CREATED = 'student_created';
case STUDENT_UPDATED = 'student_updated';
case STUDENT_STATUS_CHANGED = 'student_status_changed';
case GUARDIAN_LINKED = 'guardian_linked';
case STUDENT_ENROLLED = 'student_enrolled';
case STUDENT_ASSIGNED = 'student_assigned';
case STUDENT_PROMOTED = 'student_promoted';
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Enums;
enum StudentStatus: string
{
case PROSPECTIVE = 'prospective';
case ACTIVE = 'active';
case INACTIVE = 'inactive';
case WITHDRAWN = 'withdrawn';
case GRADUATED = 'graduated';
case ARCHIVED = 'archived';
case TRANSFERRED = 'transferred';
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class EnrollmentException extends StudentException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class GuardianException extends StudentException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class InvalidStudentStatusTransitionException extends StudentException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class PromotionException extends StudentException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class StudentException extends \RuntimeException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class StudentIdentifierCollisionException extends StudentException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class StudentNotFoundException extends StudentException
{
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Exceptions;
class UnauthorizedStudentAccessException extends StudentException
{
}
@@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\DTO\EnrollmentData;
final class DefaultEnrollmentPolicy { public function canEnroll(SchoolContext $context, EnrollmentData $data): bool { return $data->studentId>0 && $data->academicYearId>0; } }
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\Contracts\PromotionPolicyContract;
use App\Domain\SchoolCore\Students\DTO\EnrollmentView;
use App\Domain\SchoolCore\Students\DTO\PromotionEvaluationResult;
use App\Domain\SchoolCore\Students\DTO\StudentView;
final class DefaultPromotionPolicy implements PromotionPolicyContract { public function evaluate(SchoolContext $context, StudentView $student, EnrollmentView $enrollment): PromotionEvaluationResult { if($student->schoolId!==$context->schoolId||$enrollment->schoolId!==$context->schoolId){return new PromotionEvaluationResult(false,'ineligible',['wrong_school']);} return new PromotionEvaluationResult($student->status==='active',$student->status==='active'?'promoted':'manual_review'); } }
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Policies;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\Contracts\StudentIdentifierGeneratorContract;
use App\Domain\SchoolCore\Students\DTO\StudentIdentifier;
use App\Domain\SchoolCore\Students\DTO\StudentView;
final class DefaultStudentIdentifierGenerator implements StudentIdentifierGeneratorContract { public function generateForPersistedStudent(SchoolContext $context, StudentView $student): StudentIdentifier { return new StudentIdentifier(sprintf('SCH-%07d',$student->id)); } }
+3
View File
@@ -0,0 +1,3 @@
# Students
Neutral SchoolCore module boundary. Keep domain-specific vocabulary in extension modules.
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Repositories;
use App\Domain\SchoolCore\Context\SchoolContext;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
final class AssignmentRepository { public function table(): Builder { return DB::table('student_assignments'); } public function scoped(SchoolContext $context): Builder { return $this->table()->where('school_id',$context->schoolId); } }
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Repositories;
use App\Domain\SchoolCore\Context\SchoolContext;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
final class EnrollmentRepository { public function table(): Builder { return DB::table('enrollments'); } public function scoped(SchoolContext $context): Builder { return $this->table()->where('school_id',$context->schoolId); } }
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Repositories;
use App\Domain\SchoolCore\Context\SchoolContext;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
final class GuardianRepository { public function table(): Builder { return DB::table('guardians'); } public function scoped(SchoolContext $context): Builder { return $this->table()->where('school_id',$context->schoolId); } }
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Repositories;
use App\Domain\SchoolCore\Context\SchoolContext;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
final class HouseholdRepository { public function table(): Builder { return DB::table('households'); } public function scoped(SchoolContext $context): Builder { return $this->table()->where('school_id',$context->schoolId); } }
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Repositories;
use App\Domain\SchoolCore\Context\SchoolContext;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
final class StudentReadRepository { public function table(): Builder { return DB::table('students'); } public function scoped(SchoolContext $context): Builder { return $this->table()->where('school_id',$context->schoolId); } }
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Repositories;
use App\Domain\SchoolCore\Context\SchoolContext;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
final class StudentRepository { public function table(): Builder { return DB::table('students'); } public function scoped(SchoolContext $context): Builder { return $this->table()->where('school_id',$context->schoolId); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\AssignmentServiceContract; use App\Domain\SchoolCore\Students\DTO\AssignmentData; use App\Domain\SchoolCore\Students\DTO\AssignmentResult; use Illuminate\Support\Facades\DB; final class AssignmentService implements AssignmentServiceContract { public function assignStudent(SchoolContext $context,AssignmentData $data): AssignmentResult { $id=(int)DB::table('student_assignments')->insertGetId(['school_id'=>$context->schoolId,'student_id'=>$data->studentId,'assignment_type'=>$data->assignmentType,'assignable_type'=>$data->assignableType,'assignable_id'=>$data->assignableId,'effective_from'=>$data->effectiveFrom,'effective_to'=>$data->effectiveTo,'status'=>$data->status,'metadata'=>json_encode($data->metadata),'created_at'=>now(),'updated_at'=>now()]); return new AssignmentResult($id,$data->studentId,$data->status); } public function unassignStudent(SchoolContext $context,int $assignmentId,string $reason): AssignmentResult { $row=DB::table('student_assignments')->where('school_id',$context->schoolId)->where('id',$assignmentId)->first(); DB::table('student_assignments')->where('school_id',$context->schoolId)->where('id',$assignmentId)->update(['status'=>'ended','effective_to'=>now(),'updated_at'=>now()]); return new AssignmentResult($assignmentId,(int)($row->student_id??0),'ended'); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\EnrollmentServiceContract; use App\Domain\SchoolCore\Students\DTO\EnrollmentData; use App\Domain\SchoolCore\Students\DTO\EnrollmentResult; use App\Domain\SchoolCore\Students\DTO\EnrollmentView; use Illuminate\Support\Facades\DB; final class EnrollmentService implements EnrollmentServiceContract { public function enrollStudent(SchoolContext $context,EnrollmentData $data): EnrollmentResult { $id=(int)DB::table('enrollments')->insertGetId(['school_id'=>$context->schoolId,'student_id'=>$data->studentId,'academic_year_id'=>$data->academicYearId,'term_id'=>$data->termId,'grade_level_id'=>$data->gradeLevelId,'program_id'=>$data->programId,'class_group_id'=>$data->classGroupId,'status'=>$data->status,'enrolled_at'=>$data->enrollmentDate?:now(),'reason'=>$data->reason,'metadata'=>json_encode($data->metadata),'created_at'=>now(),'updated_at'=>now()]); return new EnrollmentResult(new EnrollmentView($id,$data->studentId,$context->schoolId,$data->status)); } public function withdrawStudent(SchoolContext $context,int $studentId,string $reason): EnrollmentResult { DB::table('enrollments')->where('school_id',$context->schoolId)->where('student_id',$studentId)->where('status','active')->update(['status'=>'withdrawn','withdrawn_at'=>now(),'reason'=>$reason,'updated_at'=>now()]); return new EnrollmentResult(new EnrollmentView(0,$studentId,$context->schoolId,'withdrawn')); } public function transferStudent(SchoolContext $context,int $studentId,EnrollmentData $data): EnrollmentResult { $this->withdrawStudent($context,$studentId,$data->reason??'transfer'); return $this->enrollStudent($context,$data); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\GuardianServiceContract; use App\Domain\SchoolCore\Students\Contracts\StudentLifecycleAuditLoggerContract; use App\Domain\SchoolCore\Students\DTO\CreateGuardianData; use App\Domain\SchoolCore\Students\DTO\GuardianLinkResult; use App\Domain\SchoolCore\Students\DTO\GuardianResult; use App\Domain\SchoolCore\Students\DTO\LinkGuardianData; use Illuminate\Support\Facades\DB; final class GuardianService implements GuardianServiceContract { public function __construct(private readonly StudentLifecycleAuditLoggerContract $audit){} public function createGuardian(SchoolContext $context,CreateGuardianData $data): GuardianResult { $id=(int)DB::table('guardians')->insertGetId(['school_id'=>$context->schoolId,'first_name'=>$data->firstName,'last_name'=>$data->lastName,'email'=>$data->email,'phone'=>$data->phone,'metadata'=>json_encode($data->metadata),'created_at'=>now(),'updated_at'=>now()]); $this->audit->log($context,'guardian.created','guardian',$id); return new GuardianResult($id); } public function linkGuardianToStudent(SchoolContext $context,LinkGuardianData $data): GuardianLinkResult { DB::table('student_guardian')->updateOrInsert(['school_id'=>$context->schoolId,'student_id'=>$data->studentId,'guardian_id'=>$data->guardianId],['relationship_type'=>$data->relationshipType,'emergency_contact'=>$data->emergencyContact,'pickup_authorized'=>$data->pickupAuthorized,'financial_responsibility'=>$data->financialResponsibility,'academic_contact'=>$data->academicContact,'notes'=>$data->notes,'effective_from'=>now(),'updated_at'=>now()]); $this->audit->log($context,'guardian.linked','student',$data->studentId,null,['guardian_id'=>$data->guardianId]); return new GuardianLinkResult($data->studentId,$data->guardianId,$data->relationshipType); } public function unlinkGuardianFromStudent(SchoolContext $context,int $studentId,int $guardianId,string $reason): GuardianLinkResult { DB::table('student_guardian')->where('school_id',$context->schoolId)->where('student_id',$studentId)->where('guardian_id',$guardianId)->update(['effective_to'=>now(),'updated_at'=>now()]); $this->audit->log($context,'guardian.unlinked','student',$studentId,['guardian_id'=>$guardianId],null,$reason); return new GuardianLinkResult($studentId,$guardianId,'unknown',false); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\HouseholdServiceContract; use App\Domain\SchoolCore\Students\DTO\HouseholdData; use App\Domain\SchoolCore\Students\DTO\HouseholdResult; use Illuminate\Support\Facades\DB; final class HouseholdService implements HouseholdServiceContract { public function createHousehold(SchoolContext $context,HouseholdData $data): HouseholdResult { $id=(int)DB::table('households')->insertGetId(['school_id'=>$context->schoolId,'name'=>$data->name,'primary_address'=>$data->primaryAddress,'metadata'=>json_encode($data->metadata),'created_at'=>now(),'updated_at'=>now()]); return new HouseholdResult($id); } public function addStudentToHousehold(SchoolContext $context,int $householdId,int $studentId): HouseholdResult { DB::table('household_student')->updateOrInsert(['school_id'=>$context->schoolId,'household_id'=>$householdId,'student_id'=>$studentId],['updated_at'=>now()]); return new HouseholdResult($householdId); } public function addGuardianToHousehold(SchoolContext $context,int $householdId,int $guardianId): HouseholdResult { DB::table('household_guardian')->updateOrInsert(['school_id'=>$context->schoolId,'household_id'=>$householdId,'guardian_id'=>$guardianId],['updated_at'=>now()]); return new HouseholdResult($householdId); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\PromotionPolicyContract; use App\Domain\SchoolCore\Students\Contracts\PromotionServiceContract; use App\Domain\SchoolCore\Students\DTO\BulkPromotionData; use App\Domain\SchoolCore\Students\DTO\BulkPromotionResult; use App\Domain\SchoolCore\Students\DTO\EnrollmentView; use App\Domain\SchoolCore\Students\DTO\PromotionData; use App\Domain\SchoolCore\Students\DTO\PromotionEvaluationResult; use App\Domain\SchoolCore\Students\DTO\PromotionResultData; use App\Domain\SchoolCore\Students\DTO\StudentView; use Illuminate\Support\Facades\DB; final class PromotionService implements PromotionServiceContract { public function __construct(private readonly PromotionPolicyContract $policy){} public function evaluatePromotion(SchoolContext $context,int $studentId): PromotionEvaluationResult { $s=DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->first(); return $this->policy->evaluate($context,new StudentView($studentId,$context->schoolId,$s->student_identifier??null,$s->first_name??'',$s->last_name??'',$s->status??'active'),new EnrollmentView(0,$studentId,$context->schoolId,'active')); } public function promoteStudent(SchoolContext $context,PromotionData $data): PromotionResultData { $evaluation=$this->evaluatePromotion($context,$data->studentId); if(!$evaluation->eligible&&!$data->overridePolicy){return new PromotionResultData($data->studentId,'retained',['reasons'=>$evaluation->reasons]);} return new PromotionResultData($data->studentId,'promoted'); } public function bulkPromote(SchoolContext $context,BulkPromotionData $data): BulkPromotionResult { $rows=[]; foreach($data->studentIds as $id){$rows[]=$this->promoteStudent($context,new PromotionData((int)$id,$data->fromAcademicYearId,$data->toAcademicYearId,effectiveDate:$data->effectiveDate,overridePolicy:$data->overridePolicy,overrideReason:$data->overrideReason));} return new BulkPromotionResult($rows,count($rows),0); } }
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Contracts\StudentIdentifierGeneratorContract;
final class SequentialStudentIdentifierGenerator implements StudentIdentifierGeneratorContract
{
public function generate(SchoolContext $context, int $studentId): string
{
return sprintf('%d-%06d', $context->schoolId, $studentId);
}
}
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\StudentIdentifierGeneratorContract; use App\Domain\SchoolCore\Students\DTO\StudentIdentifier; use App\Domain\SchoolCore\Students\DTO\StudentView; final class StudentIdentifierService { public function __construct(private readonly StudentIdentifierGeneratorContract $generator){} public function generate(SchoolContext $context,StudentView $student): StudentIdentifier { return $this->generator->generateForPersistedStudent($context,$student); } }
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\Contracts\StudentLifecycleAuditLoggerContract;
use Illuminate\Support\Facades\DB;
final class StudentLifecycleAuditLogger implements StudentLifecycleAuditLoggerContract { public function log(SchoolContext $context,string $action,string $entityType,int $entityId,?array $before=null,?array $after=null,?string $reason=null,?string $idempotencyKey=null): void { DB::table('student_audit_logs')->insert(['school_id'=>$context->schoolId,'actor_user_id'=>$context->actorUserId,'actor_role_snapshot'=>json_encode($context->actorRoleIds),'action'=>$action,'entity_type'=>$entityType,'entity_id'=>$entityId,'before_json'=>$before?json_encode($before):null,'after_json'=>$after?json_encode($after):null,'reason'=>$reason,'idempotency_key'=>$idempotencyKey,'created_at'=>now()]); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\StudentProfileServiceContract; use App\Domain\SchoolCore\Students\DTO\StudentProfileData; use App\Domain\SchoolCore\Students\DTO\StudentResult; use App\Domain\SchoolCore\Students\DTO\UpdateStudentData; final class StudentProfileService implements StudentProfileServiceContract { public function __construct(private readonly StudentService $students){} public function updateProfile(SchoolContext $context,StudentProfileData $data): StudentResult { return $this->students->updateStudent($context,$data->studentId,new UpdateStudentData(profileMetadata:$data->profileMetadata, updateReason:'profile update')); } }
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext; use App\Domain\SchoolCore\Students\Contracts\StudentReadServiceContract; use App\Domain\SchoolCore\Students\DTO\StudentSearchQuery; use App\Domain\SchoolCore\Students\DTO\StudentView; use App\Domain\SchoolCore\Students\Support\StudentSearchNormalizer; use Illuminate\Support\Facades\DB; final class StudentReadService implements StudentReadServiceContract { public function __construct(private readonly StudentSearchNormalizer $normalizer){} public function find(SchoolContext $context,int $studentId): ?StudentView { $s=DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->first(); return $s?new StudentView($studentId,$context->schoolId,$s->student_identifier??null,$s->first_name,$s->last_name,$s->status??'active'):null; } public function search(SchoolContext $context,StudentSearchQuery $query): array { $q=DB::table('students')->where('school_id',$context->schoolId); if($query->status){$q->where('status',$query->status);} return $q->limit($query->limit)->get()->map(fn($s)=>new StudentView((int)$s->id,$context->schoolId,$s->student_identifier??null,$s->first_name,$s->last_name,$s->status??'active'))->all(); } }
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Services;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\Contracts\StudentIdentifierGeneratorContract;
use App\Domain\SchoolCore\Students\Contracts\StudentLifecycleAuditLoggerContract;
use App\Domain\SchoolCore\Students\Contracts\StudentServiceContract as LifecycleStudentServiceContract;
use App\Domain\SchoolCore\Students\DTO\CreateStudentData;
use App\Domain\SchoolCore\Students\DTO\StudentResult;
use App\Domain\SchoolCore\Students\DTO\StudentStatusTransitionData;
use App\Domain\SchoolCore\Students\DTO\StudentView;
use App\Domain\SchoolCore\Students\DTO\UpdateStudentData;
use App\Domain\SchoolCore\Students\Exceptions\StudentNotFoundException;
use App\Domain\SchoolCore\Students\Support\StudentLifecycleStateMachine;
use App\Domain\SchoolCore\Students\Support\StudentTransactionRunner;
use Illuminate\Support\Facades\DB;
final class StudentService implements LifecycleStudentServiceContract {
public function __construct(private readonly StudentIdentifierGeneratorContract $identifiers, private readonly StudentLifecycleAuditLoggerContract $audit, private readonly StudentTransactionRunner $transactions, private readonly StudentLifecycleStateMachine $stateMachine) {}
public function createStudent(SchoolContext $context, CreateStudentData $data): StudentResult { return $this->transactions->run(function() use($context,$data){ $id=(int)DB::table('students')->insertGetId(['school_id'=>$context->schoolId,'first_name'=>$data->firstName,'middle_name'=>$data->middleName,'last_name'=>$data->lastName,'preferred_name'=>$data->preferredName,'date_of_birth'=>$data->dateOfBirth,'gender'=>$data->gender,'primary_language'=>$data->primaryLanguage,'external_id'=>$data->externalId,'status'=>$data->initialStatus,'profile_metadata'=>json_encode($data->profileMetadata),'created_at'=>now(),'updated_at'=>now()]); $view=new StudentView($id,$context->schoolId,null,$data->firstName,$data->lastName,$data->initialStatus); $identifier=$this->identifiers->generateForPersistedStudent($context,$view); DB::table('students')->where('school_id',$context->schoolId)->where('id',$id)->update(['student_identifier'=>(string)$identifier,'updated_at'=>now()]); $view=new StudentView($id,$context->schoolId,(string)$identifier,$data->firstName,$data->lastName,$data->initialStatus); $this->audit->log($context,'student.created','student',$id,null,['student_identifier'=>(string)$identifier],null,$data->idempotencyKey); return new StudentResult($view); }); }
public function updateStudent(SchoolContext $context,int $studentId,UpdateStudentData $data): StudentResult { return $this->transactions->run(function() use($context,$studentId,$data){ $before=DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->lockForUpdate()->first(); if(!$before){throw new StudentNotFoundException('Student not found in current school context.');} $updates=array_filter(['first_name'=>$data->firstName,'middle_name'=>$data->middleName,'last_name'=>$data->lastName,'preferred_name'=>$data->preferredName,'date_of_birth'=>$data->dateOfBirth,'gender'=>$data->gender,'primary_language'=>$data->primaryLanguage,'profile_metadata'=>$data->profileMetadata?json_encode($data->profileMetadata):null,'updated_at'=>now()],static fn($v)=>$v!==null); DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->update($updates); $after=DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->first(); $this->audit->log($context,'student.updated','student',$studentId,(array)$before,(array)$after,$data->updateReason); return new StudentResult(new StudentView($studentId,$context->schoolId,$after->student_identifier??null,$after->first_name,$after->last_name,$after->status??'active')); }); }
public function transitionStatus(SchoolContext $context,int $studentId,StudentStatusTransitionData $data): StudentResult { return $this->transactions->run(function() use($context,$studentId,$data){ $student=DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->lockForUpdate()->first(); if(!$student){throw new StudentNotFoundException('Student not found in current school context.');} $from=(string)($student->status??$data->fromStatus); $this->stateMachine->assertCanTransition($from,$data->toStatus); DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->update(['status'=>$data->toStatus,'updated_at'=>now()]); $this->audit->log($context,'student.status_changed','student',$studentId,['status'=>$from],['status'=>$data->toStatus],$data->reason); return new StudentResult(new StudentView($studentId,$context->schoolId,$student->student_identifier??null,$student->first_name,$student->last_name,$data->toStatus)); }); }
public function getStudent(SchoolContext $context,int $studentId): StudentView { $s=DB::table('students')->where('school_id',$context->schoolId)->where('id',$studentId)->first(); if(!$s){throw new StudentNotFoundException('Student not found in current school context.');} return new StudentView($studentId,$context->schoolId,$s->student_identifier??null,$s->first_name,$s->last_name,$s->status??'active'); }
public function create(SchoolContext $context,mixed $data): mixed { return $this->createStudent($context,$data); } public function update(SchoolContext $context,int $studentId,mixed $data): mixed { return $this->updateStudent($context,$studentId,$data); } public function find(SchoolContext $context,int $studentId): mixed { return $this->getStudent($context,$studentId); } public function archive(SchoolContext $context,int $studentId,string $reason): void { $this->transitionStatus($context,$studentId,new StudentStatusTransitionData('active','archived',$reason)); }
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Support\AppliesSchoolContext;
use App\Models\Student;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
final class StudentListQueryService
{
use AppliesSchoolContext;
/**
* @param array<string, mixed> $filters
*/
public function listStudents(SchoolContext $context, array $filters = []): LengthAwarePaginator
{
$query = Student::query();
$this->applySchoolScope($query, $context);
if (! empty($filters['search'])) {
$search = trim((string) $filters['search']);
$query->where(function ($q) use ($search): void {
$q->where('first_name', 'like', "%{$search}%")
->orWhere('last_name', 'like', "%{$search}%")
->orWhere('student_number', 'like', "%{$search}%");
});
}
return $query->orderBy('last_name')->orderBy('first_name')->paginate((int) ($filters['per_page'] ?? 25));
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Models\Student;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
final class StudentReadController
{
public function __construct(
private readonly StudentListQueryService $students,
) {
}
public function index(Request $request): JsonResponse
{
/** @var SchoolContext $context */
$context = $request->attributes->get(SchoolContext::class);
return response()->json($this->students->listStudents($context, $request->query())->toArray());
}
public function show(Request $request, Student $student): JsonResponse
{
/** @var SchoolContext $context */
$context = $request->attributes->get(SchoolContext::class);
abort_if((int) $student->school_id !== $context->schoolId, 404);
$this->authorizeStudentView($request, $student, $context);
return response()->json(['data' => $student]);
}
private function authorizeStudentView(Request $request, Student $student, SchoolContext $context): void
{
if (method_exists($request->user(), 'can') && ! $request->user()->can('view', [$student, $context])) {
abort(403);
}
}
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Support;
use App\Domain\SchoolCore\Students\Exceptions\InvalidStudentStatusTransitionException;
final class StudentLifecycleStateMachine {
private const ALLOWED=['prospective'=>['active','inactive','archived'],'active'=>['inactive','withdrawn','graduated','transferred','archived'],'inactive'=>['active','withdrawn','archived'],'withdrawn'=>['active','archived'],'transferred'=>['active','archived'],'graduated'=>['archived'],'archived'=>[]];
public function assertCanTransition(string $from,string $to): void { if($from===$to){return;} if(!in_array($to,self::ALLOWED[$from]??[],true)){throw new InvalidStudentStatusTransitionException("Invalid student status transition from {$from} to {$to}.");} }
}
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Support;
final class StudentSearchNormalizer { public function normalize(?string $value): ?string { $value=trim((string)$value); return $value===''?null:mb_strtolower(preg_replace('/\s+/',' ',$value)); } }
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\SchoolCore\Students\Support;
use Illuminate\Support\Facades\DB;
final class StudentTransactionRunner { public function run(callable $callback): mixed { return DB::transaction($callback); } }