makeRecord(StudentPromotionRecord::STATUS_AWAITING_PARENT); $service = $this->makeService(); $updated = $service->transition( $record, StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, 42, 'parent began enrollment' ); $this->assertSame(StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, $updated->promotion_status); $this->assertDatabaseHas('promotion_audit_log', [ 'promotion_id' => $record->getKey(), 'action_type' => 'status_changed', 'old_value' => StudentPromotionRecord::STATUS_AWAITING_PARENT, 'new_value' => StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, 'user_id' => 42, ]); } public function test_disallowed_transition_throws(): void { $record = $this->makeRecord(StudentPromotionRecord::STATUS_NOT_REVIEWED); $service = $this->makeService(); $this->expectException(RuntimeException::class); $service->transition($record, StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, 1); } public function test_force_status_bypasses_transition_map(): void { $record = $this->makeRecord(StudentPromotionRecord::STATUS_NOT_REVIEWED); $service = $this->makeService(); $updated = $service->forceStatus($record, StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, 7, 'manual fix'); $this->assertSame(StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, $updated->promotion_status); $this->assertDatabaseHas('promotion_audit_log', [ 'promotion_id' => $record->getKey(), 'action_type' => 'manual_override', 'field' => 'promotion_status', 'old_value' => StudentPromotionRecord::STATUS_NOT_REVIEWED, 'new_value' => StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, ]); } public function test_no_op_transition_returns_record_unchanged(): void { $record = $this->makeRecord(StudentPromotionRecord::STATUS_AWAITING_PARENT); $service = $this->makeService(); $updated = $service->transition( $record, StudentPromotionRecord::STATUS_AWAITING_PARENT, 1 ); $this->assertSame(StudentPromotionRecord::STATUS_AWAITING_PARENT, $updated->promotion_status); $this->assertDatabaseMissing('promotion_audit_log', [ 'promotion_id' => $record->getKey(), 'action_type' => 'status_changed', ]); } private function makeService(): PromotionStatusService { return new PromotionStatusService(new PromotionAuditService()); } private function makeRecord(string $status): StudentPromotionRecord { $studentId = DB::table('students')->insertGetId([ 'school_id' => 'S-1', 'firstname' => 'Test', 'lastname' => 'Student', 'parent_id' => 1, 'year_of_registration' => '2025', 'school_year' => '2025-2026', 'semester' => 'Fall', ]); return StudentPromotionRecord::query()->create([ 'student_id' => $studentId, 'parent_id' => 1, 'current_school_year' => '2025-2026', 'next_school_year' => '2026-2027', 'promotion_status' => $status, 'enrollment_required' => true, 'enrollment_status' => StudentPromotionRecord::ENROLLMENT_NOT_STARTED, ]); } }