fix parent pages and teachers and admin
API CI/CD / Validate (composer + pint) (push) Successful in 3m8s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Test (PHPUnit) (push) Failing after 6m5s
API CI/CD / Security audit (push) Failing after 52s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped

This commit is contained in:
root
2026-07-09 13:56:22 -04:00
parent 21c9322127
commit 02ba2fe6b6
34 changed files with 2306 additions and 99 deletions
@@ -152,28 +152,49 @@ class SchoolYearControllerTest extends TestCase
$this->assertSame([502], $row['credit_invoice_ids']);
}
public function test_credit_only_parent_is_reported_as_credit_not_unpaid_carryover(): void
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' => 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');
@@ -182,18 +203,53 @@ class SchoolYearControllerTest extends TestCase
$response->assertOk()
->assertJsonPath('data.summary.parents_with_positive_balance', 1)
->assertJsonPath('data.summary.parents_with_credit_balance', 1)
->assertJsonPath('data.summary.parents_with_credit_balance', 0)
->assertJsonPath('data.summary.total_positive_unpaid_balance', 200)
->assertJsonPath('data.summary.total_credit_balance', 50);
->assertJsonPath('data.summary.total_credit_balance', 0)
->assertJsonPath('data.summary.parent_count', 1)
->assertJsonPath('data.summary.total_transferred', 200);
$creditRow = collect($response->json('data.rows'))
->first(fn (array $row): bool => (int) $row['parent_id'] === 12);
$rows = collect($response->json('data.rows'));
$this->assertNotNull($creditRow);
$this->assertSame(0.0, (float) $creditRow['positive_unpaid_balance']);
$this->assertSame(50.0, (float) $creditRow['credit_balance']);
$this->assertSame(0.0, (float) $creditRow['amount_to_transfer']);
$this->assertSame([503], $creditRow['credit_invoice_ids']);
$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
@@ -567,6 +623,139 @@ class SchoolYearControllerTest extends TestCase
]);
}
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();