seedParent(); $studentId = $this->seedStudent($parent->id); $this->seedPromotionRecord($studentId, $parent->id); Sanctum::actingAs($parent); $response = $this->getJson('/api/v1/parents/promotions'); $response->assertOk(); $response->assertJsonPath('ok', true); $response->assertJsonPath('records.0.parent_action_required', true); $response->assertJsonPath('records.0.promoted_level', 'Grade 4'); } public function test_parent_completes_full_enrollment_flow(): void { $parent = $this->seedParent(); $studentId = $this->seedStudent($parent->id); $promotionId = $this->seedPromotionRecord($studentId, $parent->id); Sanctum::actingAs($parent); // Start enrollment $start = $this->postJson('/api/v1/parents/promotions/' . $promotionId . '/start'); $start->assertOk(); $start->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_ENROLLMENT_STARTED); // Complete the checklist incrementally $update = $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ 'info_confirmed' => true, 'documents_uploaded' => true, 'agreement_accepted' => true, ]); $update->assertOk(); $update->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_ENROLLMENT_STARTED); // Final step finalises promotion $finish = $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ 'payment_completed' => true, ]); $finish->assertOk(); $finish->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED); $this->assertDatabaseHas('enrollments', [ 'student_id' => $studentId, 'school_year' => '2026-2027', 'enrollment_status' => 'enrolled', ]); } public function test_parent_cannot_view_other_parents_promotion(): void { $parentA = $this->seedParent('A', 'a@example.com'); $parentB = $this->seedParent('B', 'b@example.com'); $studentId = $this->seedStudent($parentB->id); $promotionId = $this->seedPromotionRecord($studentId, $parentB->id); Sanctum::actingAs($parentA); $response = $this->getJson('/api/v1/parents/promotions/' . $promotionId); $response->assertStatus(403); } public function test_unauthenticated_request_is_unauthorized(): void { $response = $this->getJson('/api/v1/parents/promotions'); $response->assertStatus(401); } private function seedParent(string $name = 'Parent', string $email = 'parent@example.com'): User { DB::table('roles')->insertOrIgnore([ ['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1], ]); $userId = DB::table('users')->insertGetId([ 'firstname' => $name, 'lastname' => 'User', 'cellphone' => '5550000000', 'email' => $email, 'address_street' => '1 Way', 'city' => 'C', 'state' => 'S', 'zip' => '00000', 'accept_school_policy' => 1, 'password' => bcrypt('secret'), 'user_type' => 'primary', 'school_year' => '2025-2026', 'semester' => 'Fall', 'status' => 'Active', ]); DB::table('user_roles')->insert([ 'user_id' => $userId, 'role_id' => 2, ]); return User::query()->findOrFail($userId); } private function seedStudent(int $parentId): int { return (int) DB::table('students')->insertGetId([ 'school_id' => 'S-1', 'firstname' => 'Sara', 'lastname' => 'Test', 'parent_id' => $parentId, 'year_of_registration' => '2025', 'school_year' => '2025-2026', 'semester' => 'Fall', ]); } private function seedPromotionRecord(int $studentId, int $parentId): int { $record = StudentPromotionRecord::query()->create([ 'student_id' => $studentId, 'parent_id' => $parentId, 'current_school_year' => '2025-2026', 'next_school_year' => '2026-2027', 'promotion_status' => StudentPromotionRecord::STATUS_AWAITING_PARENT, 'enrollment_required' => true, 'enrollment_status' => StudentPromotionRecord::ENROLLMENT_NOT_STARTED, 'current_level_name' => 'Grade 3', 'promoted_level_name' => 'Grade 4', 'enrollment_deadline' => now()->addDays(30)->toDateString(), ]); return (int) $record->getKey(); } }