seedClosureData(); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years/1/preview-close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $response->assertOk() ->assertJsonPath('data.validation.can_close', true) ->assertJsonPath('data.promotion_preview.summary.promote', 1) ->assertJsonPath('data.parent_balances.summary.total_old_unpaid_balance', 200); $balanceRows = collect($response->json('data.parent_balances.rows')); $parentRow = $balanceRows->first(fn (array $row): bool => (int) $row['parent_id'] === 10); $this->assertCount(1, $balanceRows); $this->assertNotNull($parentRow); $this->assertSame(200.0, (float) $parentRow['amount_to_transfer']); $this->assertSame('promote', $response->json('data.promotion_preview.rows.0.promotion_action')); } public function test_name_based_school_year_management_routes_use_school_year_query(): void { $this->seedClosureData(); $this->actingAs(User::query()->findOrFail(1), 'api'); $this->getJson('/api/v1/school-years/summary?school_year=2025-2026') ->assertOk() ->assertJsonPath('data.school_year.name', '2025-2026'); $this->getJson('/api/v1/school-years/promotion-preview?school_year=2025-2026&to_school_year=2026-2027') ->assertOk() ->assertJsonPath('data.current_school_year', '2025-2026') ->assertJsonPath('data.target_school_year', '2026-2027'); $this->postJson('/api/v1/school-years/preview-close?school_year=2025-2026', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ])->assertOk() ->assertJsonPath('data.school_year.name', '2025-2026') ->assertJsonPath('data.parent_balances.summary.total_old_unpaid_balance', 200); } public function test_preview_close_excludes_zero_invoice_balance_parent_even_when_parent_account_is_stale(): void { $this->seedClosureData(); $this->seedZeroBalanceParentWithStaleAccount(); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years/1/preview-close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $response->assertOk() ->assertJsonPath('data.validation.can_close', true) ->assertJsonPath('data.parent_balances.summary.total_old_unpaid_balance', 200) ->assertJsonPath('data.parent_balances.summary.net_balance_to_transfer', 200); $balanceRows = collect($response->json('data.parent_balances.rows')); $this->assertTrue( $balanceRows->contains(fn (array $row): bool => (int) $row['parent_id'] === 10 && (float) $row['amount_to_transfer'] === 200.0) ); $this->assertFalse( $balanceRows->contains(fn (array $row): bool => (int) $row['parent_id'] === 11), 'Parent with invoice balance 0 must not appear in closure balance preview, even when parent_accounts.current_balance is stale.' ); } public function test_positive_balance_is_not_hidden_by_credit_balance(): void { $this->seedClosureData(); DB::table('invoices')->insert([ 'id' => 502, 'parent_id' => 10, 'invoice_number' => 'INV-502-CREDIT', 'total_amount' => 0, 'balance' => -200, 'paid_amount' => 200, 'has_discount' => 0, 'issue_date' => '2026-05-11', 'due_date' => '2026-06-10', 'status' => 'credit', 'description' => 'Credit balance', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), 'updated_by' => 1, 'semester' => 'Fall', ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years/preview-close?school_year=2025-2026', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $response->assertOk() ->assertJsonPath('data.parent_balances.summary.parents_with_positive_balance', 1) ->assertJsonPath('data.parent_balances.summary.total_positive_unpaid_balance', 200) ->assertJsonPath('data.parent_balances.summary.parents_with_credit_balance', 1) ->assertJsonPath('data.parent_balances.summary.total_credit_balance', 200) ->assertJsonPath('data.parent_balances.summary.net_balance_impact', 0); $row = collect($response->json('data.parent_balances.rows')) ->first(fn (array $row): bool => (int) $row['parent_id'] === 10); $this->assertSame(200.0, (float) $row['positive_unpaid_balance']); $this->assertSame(200.0, (float) $row['credit_balance']); $this->assertSame(0.0, (float) $row['net_balance']); $this->assertSame(200.0, (float) $row['amount_to_transfer']); $this->assertSame([500], $row['positive_invoice_ids']); $this->assertSame([502], $row['credit_invoice_ids']); } public function test_parent_balance_report_uses_only_positive_invoice_balances(): void { $this->seedClosureData(); $this->seedParent(12, 'Credit', 'Only', 'credit@example.com'); $this->seedParent(13, 'Discount', 'Paid', 'discount-paid@example.com'); DB::table('invoices')->insert([ [ 'id' => 503, 'parent_id' => 12, 'invoice_number' => 'INV-503-CREDIT', 'total_amount' => 0, 'balance' => -50, 'paid_amount' => 50, 'has_discount' => 0, 'issue_date' => '2026-05-11', 'due_date' => '2026-06-10', 'status' => 'credit', 'description' => 'Credit only', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), 'updated_by' => 1, 'semester' => 'Fall', ], [ 'id' => 505, 'parent_id' => 13, 'invoice_number' => 'INV-505-DISCOUNT-PAID', 'total_amount' => 500, 'balance' => 0, 'paid_amount' => 500, 'has_discount' => 1, 'issue_date' => '2026-05-11', 'due_date' => '2026-06-10', 'status' => 'paid', 'description' => 'Discount paid invoice', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), 'updated_by' => 1, 'semester' => 'Fall', ], ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->getJson('/api/v1/school-years/parent-balances?school_year=2025-2026&to_school_year=2026-2027'); $response->assertOk() ->assertJsonPath('data.summary.parents_with_positive_balance', 1) ->assertJsonPath('data.summary.parents_with_credit_balance', 0) ->assertJsonPath('data.summary.total_positive_unpaid_balance', 200) ->assertJsonPath('data.summary.total_credit_balance', 0) ->assertJsonPath('data.summary.parent_count', 1) ->assertJsonPath('data.summary.total_transferred', 200); $rows = collect($response->json('data.rows')); $this->assertTrue($rows->contains(fn (array $row): bool => (int) $row['parent_id'] === 10)); $this->assertSame(200.0, (float) $rows->first(fn (array $row): bool => (int) $row['parent_id'] === 10)['old_unpaid_amount']); $this->assertFalse($rows->contains(fn (array $row): bool => (int) $row['parent_id'] === 12)); $this->assertFalse($rows->contains(fn (array $row): bool => (int) $row['parent_id'] === 13)); } public function test_closing_report_unpaid_balance_totals_are_recomputed_from_positive_invoice_balances(): void { $this->seedClosureData(); DB::table('school_years')->where('id', 1)->update([ 'status' => 'closed', 'closed_at' => now(), 'closed_by' => 1, ]); DB::table('audit_logs')->insert([ 'user_id' => 1, 'action' => 'school_year_closed', 'table_name' => 'school_years', 'record_id' => 1, 'old_value' => null, 'new_value' => null, 'metadata' => json_encode([ 'new_school_year' => '2026-2027', 'balance_counts' => [ 'transferred_parents' => 48, 'transferred_amount' => 9999, ], ]), 'created_at' => now(), ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $this->getJson('/api/v1/school-years/reports/closing?school_year=2025-2026') ->assertOk() ->assertJsonPath('data.totals.parents_with_unpaid_balances', 1) ->assertJsonPath('data.totals.total_unpaid_balance_transferred', 200); } public function test_pending_or_draft_invoice_blocks_close_but_is_not_transferred(): void { $this->seedClosureData(); DB::table('invoices')->insert([ 'id' => 504, 'parent_id' => 10, 'invoice_number' => 'INV-504-DRAFT', 'total_amount' => 100, 'balance' => 100, 'paid_amount' => 0, 'has_discount' => 0, 'issue_date' => '2026-05-11', 'due_date' => '2026-06-10', 'status' => 'draft', 'description' => 'Draft invoice', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), 'updated_by' => 1, 'semester' => 'Fall', ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years/preview-close?school_year=2025-2026', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $response->assertOk() ->assertJsonPath('data.validation.can_close', false) ->assertJsonPath('data.parent_balances.summary.total_positive_unpaid_balance', 200); $this->assertStringContainsString( 'invoices are still in draft status', implode(' ', $response->json('data.validation.errors')) ); } public function test_close_school_year_does_not_transfer_zero_invoice_balance_parent_even_when_parent_account_is_stale(): void { $this->seedClosureData(); $this->seedZeroBalanceParentWithStaleAccount(); $this->actingAs(User::query()->findOrFail(1), 'api'); $this->postJson('/api/v1/school-years/1/close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ])->assertOk(); $this->assertDatabaseHas('parent_balance_transfers', [ 'parent_id' => 10, 'from_school_year' => '2025-2026', 'to_school_year' => '2026-2027', 'amount' => 200.00, 'status' => 'transferred', ]); $this->assertDatabaseMissing('parent_balance_transfers', [ 'parent_id' => 11, 'from_school_year' => '2025-2026', 'to_school_year' => '2026-2027', ]); $this->assertDatabaseMissing('invoices', [ 'parent_id' => 11, 'school_year' => '2026-2027', 'description' => 'Opening balance transferred from 2025-2026', ]); } public function test_close_does_not_mutate_old_year_parent_account(): void { $this->seedClosureData(); DB::table('parent_accounts')->insert([ 'parent_id' => 10, 'school_year' => '2025-2026', 'opening_balance' => 55, 'current_balance' => 77, 'created_at' => now(), 'updated_at' => now(), ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $this->postJson('/api/v1/school-years/close?school_year=2025-2026', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ])->assertOk(); $this->assertDatabaseHas('parent_accounts', [ 'parent_id' => 10, 'school_year' => '2025-2026', 'opening_balance' => 55, 'current_balance' => 77, ]); $this->assertDatabaseHas('parent_accounts', [ 'parent_id' => 10, 'school_year' => '2026-2027', 'opening_balance' => 200, 'current_balance' => 200, ]); } public function test_existing_balance_transfer_conflict_is_visible_in_preview_and_blocks_close(): void { $this->seedClosureData(); DB::table('parent_balance_transfers')->insert([ 'parent_id' => 10, 'from_school_year' => '2025-2026', 'to_school_year' => '2026-2027', 'amount' => 200, 'status' => 'transferred', 'source_summary_json' => json_encode(['source_invoice_ids' => [500]]), 'created_by' => 1, 'created_at' => now(), 'updated_at' => now(), ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years/preview-close?school_year=2025-2026', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $response->assertOk() ->assertJsonPath('data.validation.can_close', false) ->assertJsonPath('data.parent_balances.summary.existing_transfer_conflicts', 1) ->assertJsonPath('data.parent_balances.rows.0.existing_transfer_conflict', true); } public function test_parent_cannot_close_school_year(): void { $this->seedClosureData(); $this->actingAs(User::query()->findOrFail(10), 'api'); $this->postJson('/api/v1/school-years/1/close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ])->assertForbidden(); } public function test_school_year_closure_routes_have_explicit_permission_middleware(): void { $routes = collect(app('router')->getRoutes()->getRoutes()) ->filter(fn ($route) => str_starts_with($route->uri(), 'api/v1/school-years')); $missing = []; foreach ($routes as $route) { $uri = $route->uri(); $middleware = $route->gatherMiddleware(); $isContextRoute = in_array($uri, [ 'api/v1/school-years', 'api/v1/school-years/current', 'api/v1/school-years/options', ], true); $hasPermission = $isContextRoute || collect($middleware)->contains(fn (string $middleware): bool => str_starts_with($middleware, 'perm:')); $hasAuth = collect($middleware)->contains(fn (string $middleware): bool => str_contains($middleware, 'multi.auth') || str_contains($middleware, 'auth')); $hasAccountGuard = in_array('account.active', $middleware, true); if (! $hasPermission || ! $hasAuth || ! $hasAccountGuard) { $missing[] = implode('|', $route->methods()).' '.$route->uri(); } } $this->assertSame([], $missing, 'School-year routes missing auth/account/permission middleware: '.implode(', ', $missing)); } public function test_active_user_can_load_school_year_context_list_without_school_year_permission(): void { $this->seedClosureData(); $this->actingAs(User::query()->findOrFail(10), 'api'); $response = $this->getJson('/api/v1/school-years'); $response->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('current_school_year', '2025-2026'); $this->postJson('/api/v1/school-years', [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ])->assertForbidden(); } public function test_admin_can_create_draft_school_year_via_api(): void { $this->seedClosureData(); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years', [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ]); $response->assertCreated() ->assertJsonPath('data.name', '2026-2027') ->assertJsonPath('data.status', 'draft'); $this->assertDatabaseHas('school_years', [ 'name' => '2026-2027', 'status' => 'draft', 'is_current' => 0, ]); } public function test_admin_can_update_draft_school_year_via_api(): void { $this->seedClosureData(); DB::table('school_years')->insert([ 'id' => 2, 'name' => '2027-2028', 'start_date' => '2027-09-01', 'end_date' => '2028-06-30', 'status' => 'draft', 'is_current' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->patchJson('/api/v1/school-years/2', [ 'name' => '2028-2029', 'start_date' => '2028-09-01', 'end_date' => '2029-06-30', ]); $response->assertOk() ->assertJsonPath('data.name', '2028-2029') ->assertJsonPath('data.start_date', '2028-09-01'); $this->assertDatabaseHas('school_years', [ 'id' => 2, 'name' => '2028-2029', 'start_date' => '2028-09-01 00:00:00', 'end_date' => '2029-06-30 00:00:00', ]); } public function test_admin_can_update_school_year_by_name_via_api(): void { $this->seedClosureData(); DB::table('school_years')->insert([ 'id' => 2, 'name' => '2027-2028', 'start_date' => '2027-09-01', 'end_date' => '2028-06-30', 'status' => 'draft', 'is_current' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->patchJson('/api/v1/school-years/selected?school_year=2027-2028', [ 'name' => '2028-2029', 'start_date' => '2028-09-01', 'end_date' => '2029-06-30', 'school_year' => '2027-2028', ]); $response->assertOk() ->assertJsonPath('data.name', '2028-2029') ->assertJsonPath('data.start_date', '2028-09-01'); $this->assertDatabaseHas('school_years', [ 'id' => 2, 'name' => '2028-2029', 'start_date' => '2028-09-01 00:00:00', 'end_date' => '2029-06-30 00:00:00', ]); } public function test_close_school_year_creates_new_active_year_enrollment_and_balance_transfer(): void { $this->seedClosureData(); $this->actingAs(User::query()->findOrFail(1), 'api'); $response = $this->postJson('/api/v1/school-years/1/close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ]); $response->assertOk() ->assertJsonPath('data.closed_school_year.status', 'closed') ->assertJsonPath('data.new_school_year.status', 'active'); $this->assertDatabaseHas('school_years', [ 'name' => '2025-2026', 'status' => 'closed', 'is_current' => 0, ]); $this->assertDatabaseHas('school_years', [ 'name' => '2026-2027', 'status' => 'active', 'is_current' => 1, ]); $this->assertDatabaseHas('configuration', [ 'config_key' => 'school_year', 'config_value' => '2026-2027', ]); $this->assertDatabaseHas('enrollments', [ 'student_id' => 100, 'parent_id' => 10, 'school_year' => '2026-2027', 'enrollment_status' => 'enrolled', ]); $this->assertDatabaseHas('parent_balance_transfers', [ 'parent_id' => 10, 'from_school_year' => '2025-2026', 'to_school_year' => '2026-2027', 'amount' => 200.00, 'status' => 'transferred', ]); $this->assertDatabaseHas('invoices', [ 'parent_id' => 10, 'school_year' => '2026-2027', 'total_amount' => 200.00, 'balance' => 200.00, 'description' => 'Opening balance transferred from 2025-2026', ]); } public function test_passing_age_17_student_graduates_and_is_not_enrolled_next_year(): void { $this->seedClosureData(); DB::table('students')->where('id', 100)->update(['age' => 17]); $this->actingAs(User::query()->findOrFail(1), 'api'); $preview = $this->postJson('/api/v1/school-years/1/preview-close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $preview->assertOk() ->assertJsonPath('data.promotion_preview.summary.promote', 0) ->assertJsonPath('data.promotion_preview.summary.graduate', 1) ->assertJsonPath('data.promotion_preview.rows.0.promotion_action', 'graduate'); $this->postJson('/api/v1/school-years/1/close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ])->assertOk(); $this->assertDatabaseMissing('enrollments', [ 'student_id' => 100, 'school_year' => '2026-2027', ]); $this->assertDatabaseMissing('student_class', [ 'student_id' => 100, 'school_year' => '2026-2027', ]); } public function test_kindergarten_student_age_six_next_year_promotes_to_grade_one_not_arabic(): void { $this->seedClosureData(); DB::table('classes')->insert([ ['id' => 1, 'class_name' => '1', 'schedule' => 'Sunday 9:00', 'school_year' => '2026-2027', 'semester' => 'Fall', 'capacity' => 30], ['id' => 13, 'class_name' => 'KG', 'schedule' => 'Sunday 9:00', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 30], ['id' => 14, 'class_name' => 'Arabic', 'schedule' => 'Sunday 11:00', 'school_year' => '2026-2027', 'semester' => 'Fall', 'capacity' => 30], ]); DB::table('classSection')->insert([ [ 'class_section_id' => 1, 'class_section_name' => 'KG', 'class_id' => 13, 'semester' => 'Fall', 'school_year' => '2025-2026', ], [ 'class_section_id' => 10, 'class_section_name' => '1A', 'class_id' => 1, 'semester' => 'Fall', 'school_year' => '2026-2027', ], [ 'class_section_id' => 140, 'class_section_name' => 'Arabic A', 'class_id' => 14, 'semester' => 'Fall', 'school_year' => '2026-2027', ], ]); DB::table('students')->where('id', 100)->update(['age' => 5]); DB::table('student_class')->where('student_id', 100)->where('school_year', '2025-2026')->update([ 'class_section_id' => 1, 'updated_at' => now(), ]); DB::table('enrollments')->where('student_id', 100)->where('school_year', '2025-2026')->update([ 'class_section_id' => 1, 'updated_at' => now(), ]); $this->actingAs(User::query()->findOrFail(1), 'api'); $preview = $this->postJson('/api/v1/school-years/1/preview-close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, ]); $preview->assertOk() ->assertJsonPath('data.promotion_preview.rows.0.promotion_action', 'promote') ->assertJsonPath('data.promotion_preview.rows.0.current_grade', 'KG') ->assertJsonPath('data.promotion_preview.rows.0.target_grade', '1') ->assertJsonPath('data.promotion_preview.rows.0.target_class_section_id', 10); $this->postJson('/api/v1/school-years/1/close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ])->assertOk(); $this->assertDatabaseHas('enrollments', [ 'student_id' => 100, 'school_year' => '2026-2027', 'class_section_id' => 10, ]); $this->assertDatabaseHas('student_class', [ 'student_id' => 100, 'school_year' => '2026-2027', 'class_section_id' => 10, ]); $this->assertDatabaseMissing('enrollments', [ 'student_id' => 100, 'school_year' => '2026-2027', 'class_section_id' => 140, ]); } public function test_closed_school_year_blocks_legacy_invoice_generation(): void { $this->seedClosureData(); $this->actingAs(User::query()->findOrFail(1), 'api'); $this->postJson('/api/v1/school-years/1/close', [ 'new_school_year' => [ 'name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30', ], 'transfer_unpaid_balances' => true, 'confirmation' => 'CLOSE 2025-2026', ])->assertOk(); $response = $this->postJson('/api/v1/finance/invoices/generate', [ 'parent_id' => 10, 'school_year' => '2025-2026', ]); $response->assertStatus(409) ->assertJsonPath('message', 'School year 2025-2026 is closed and read-only.'); } private function seedZeroBalanceParentWithStaleAccount(): void { DB::table('users')->insert([ 'id' => 11, 'school_id' => 1, 'firstname' => 'Zero', 'lastname' => 'Balance', 'cellphone' => '5555555556', 'email' => 'zero@example.com', 'address_street' => '123 Main', 'city' => 'City', 'state' => 'ST', 'zip' => '12345', 'accept_school_policy' => 1, 'is_verified' => 1, 'status' => 'Active', 'is_suspended' => 0, 'failed_attempts' => 0, 'password' => bcrypt('secret'), 'semester' => 'Fall', 'school_year' => '2025-2026', ]); DB::table('user_roles')->insert([ 'user_id' => 11, 'role_id' => 2, ]); $parentAccount = [ 'parent_id' => 11, 'school_year' => '2025-2026', 'opening_balance' => 999, 'current_balance' => 999, ]; if (Schema::hasColumn('parent_accounts', 'created_at')) { $parentAccount['created_at'] = now(); } if (Schema::hasColumn('parent_accounts', 'updated_at')) { $parentAccount['updated_at'] = now(); } DB::table('parent_accounts')->insert($parentAccount); DB::table('invoices')->insert([ 'id' => 501, 'parent_id' => 11, 'invoice_number' => 'INV-501', 'total_amount' => 500, 'balance' => 0, 'paid_amount' => 500, 'has_discount' => 0, 'issue_date' => '2026-05-10', 'due_date' => '2026-06-10', 'status' => 'overdue', 'description' => 'Paid invoice with stale overdue status', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), 'updated_by' => 1, 'semester' => 'Fall', ]); } private function seedParent(int $id, string $firstname, string $lastname, string $email): void { DB::table('users')->insert([ 'id' => $id, 'school_id' => $id, 'firstname' => $firstname, 'lastname' => $lastname, 'cellphone' => '5555555555', 'email' => $email, 'address_street' => '123 Main', 'city' => 'City', 'state' => 'ST', 'zip' => '12345', 'accept_school_policy' => 1, 'is_verified' => 1, 'status' => 'Active', 'is_suspended' => 0, 'failed_attempts' => 0, 'password' => bcrypt('secret'), 'semester' => 'Fall', 'school_year' => '2025-2026', ]); DB::table('user_roles')->insert([ 'user_id' => $id, 'role_id' => 2, ]); } private function seedClosureData(): void { DB::table('configuration')->insert([ ['config_key' => 'school_year', 'config_value' => '2025-2026'], ['config_key' => 'semester', 'config_value' => 'Fall'], ]); DB::table('roles')->insert([ ['id' => 1, 'name' => 'administrator', 'slug' => 'administrator', 'is_active' => 1], ['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1], ]); DB::table('users')->insert([ [ 'id' => 1, 'school_id' => 1, 'firstname' => 'Admin', 'lastname' => 'User', 'cellphone' => '5555555555', 'email' => 'admin@example.com', 'address_street' => '123 Main', 'city' => 'City', 'state' => 'ST', 'zip' => '12345', 'accept_school_policy' => 1, 'is_verified' => 1, 'status' => 'Active', 'is_suspended' => 0, 'failed_attempts' => 0, 'password' => bcrypt('secret'), 'semester' => 'Fall', 'school_year' => '2025-2026', ], [ 'id' => 10, 'school_id' => 1, 'firstname' => 'Parent', 'lastname' => 'User', 'cellphone' => '5555555555', 'email' => 'parent@example.com', 'address_street' => '123 Main', 'city' => 'City', 'state' => 'ST', 'zip' => '12345', 'accept_school_policy' => 1, 'is_verified' => 1, 'status' => 'Active', 'is_suspended' => 0, 'failed_attempts' => 0, 'password' => bcrypt('secret'), 'semester' => 'Fall', 'school_year' => '2025-2026', ], ]); DB::table('user_roles')->insert([ ['user_id' => 1, 'role_id' => 1], ['user_id' => 10, 'role_id' => 2], ]); $permissions = [ 'school_year.view', 'school_year.create', 'school_year.manage', 'school_year.close', 'school_year.reopen', 'school_year.archive', 'school_year.select', 'student_enrollment.promote', 'finance.balance_transfer.view', ]; foreach ($permissions as $permission) { DB::table('permissions')->updateOrInsert( ['name' => $permission], [ 'description' => 'Test permission '.$permission, 'created_at' => now(), 'updated_at' => now(), ] ); $permissionId = (int) DB::table('permissions')->where('name', $permission)->value('id'); DB::table('role_permissions')->insert([ 'role_id' => 1, 'permission_id' => $permissionId, 'can_create' => 1, 'can_read' => 1, 'can_update' => 1, 'can_delete' => 1, 'can_manage' => 1, 'created_at' => now(), 'updated_at' => now(), ]); } DB::table('school_years')->insert([ 'id' => 1, 'name' => '2025-2026', 'start_date' => '2025-09-01', 'end_date' => '2026-06-30', 'status' => 'active', 'is_current' => 1, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('students')->insert([ 'id' => 100, 'parent_id' => 10, 'firstname' => 'Sara', 'lastname' => 'Student', 'school_id' => 'S-100', 'age' => 9, 'gender' => 'Female', 'is_active' => 1, 'photo_consent' => 1, 'year_of_registration' => '2025', 'school_year' => '2025-2026', 'semester' => 'Fall', ]); DB::table('classes')->insert([ ['id' => 3, 'class_name' => 'Grade 3', 'schedule' => 'Sunday 9:00', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 30], ['id' => 4, 'class_name' => 'Grade 4', 'schedule' => 'Sunday 10:00', 'school_year' => '2026-2027', 'semester' => 'Fall', 'capacity' => 30], ]); DB::table('classSection')->insert([ [ 'class_section_id' => 301, 'class_section_name' => '3A', 'class_id' => 3, 'semester' => 'Fall', 'school_year' => '2025-2026', ], [ 'class_section_id' => 401, 'class_section_name' => '4A', 'class_id' => 4, 'semester' => 'Fall', 'school_year' => '2026-2027', ], ]); DB::table('student_class')->insert([ 'student_id' => 100, 'class_section_id' => 301, 'school_year' => '2025-2026', 'semester' => 'Fall', 'is_event_only' => 0, ]); DB::table('enrollments')->insert([ 'student_id' => 100, 'class_section_id' => 301, 'parent_id' => 10, 'enrollment_date' => '2025-09-05', 'enrollment_status' => 'enrolled', 'withdrawal_date' => null, 'is_withdrawn' => 0, 'admission_status' => 'accepted', 'semester' => 'Fall', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('student_decisions')->insert([ 'student_id' => 100, 'school_year' => '2025-2026', 'class_section_name' => '3A', 'year_score' => 85, 'decision' => 'promoted', 'source' => 'manual', 'notes' => 'Ready for promotion', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('invoices')->insert([ 'id' => 500, 'parent_id' => 10, 'invoice_number' => 'INV-500', 'total_amount' => 500, 'balance' => 200, 'paid_amount' => 300, 'has_discount' => 0, 'issue_date' => '2026-05-10', 'due_date' => '2026-06-10', 'status' => 'unpaid', 'description' => 'Tuition balance', 'school_year' => '2025-2026', 'created_at' => now(), 'updated_at' => now(), 'updated_by' => 1, 'semester' => 'Fall', ]); } }