reconstruction of the project

This commit is contained in:
root
2026-03-08 16:33:24 -04:00
parent 23b7db1107
commit c8de5f7edc
9157 changed files with 77877 additions and 1073823 deletions
@@ -0,0 +1,124 @@
<?php
namespace Tests\Feature\Api\V1\Admin;
use App\Models\User;
use App\Services\Administrator\AdministratorAbsenceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Mockery;
use Tests\TestCase;
class AdministratorAbsenceControllerTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function test_guest_cannot_access_absence_index(): void
{
$response = $this->getJson('/api/administrator/absence');
$response->assertStatus(401);
}
public function test_authenticated_user_can_get_absence_index(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$mock = Mockery::mock(AdministratorAbsenceService::class);
$mock->shouldReceive('getAbsenceFormData')
->once()
->with($user->id)
->andReturn([
'admin_name' => 'Test Admin',
'semester' => 'Fall',
'schoolYear' => '2025-2026',
'existing' => [],
'availableDates' => ['2026-03-08'],
]);
$this->app->instance(AdministratorAbsenceService::class, $mock);
$response = $this->getJson('/api/administrator/absence');
$response->assertOk()
->assertJson([
'admin_name' => 'Test Admin',
'semester' => 'Fall',
'schoolYear' => '2025-2026',
]);
}
public function test_absence_store_requires_reason(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->postJson('/api/administrator/absence', [
'dates' => ['2026-03-08'],
'reason_type' => 'vacation',
'reason' => '',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['reason']);
}
public function test_absence_store_requires_valid_date_format(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->postJson('/api/administrator/absence', [
'dates' => ['03/08/2026'],
'reason_type' => 'vacation',
'reason' => 'Family trip',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['dates.0']);
}
public function test_absence_store_calls_service_and_returns_success(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'dates' => ['2026-03-08'],
'reason_type' => 'vacation',
'reason' => 'Family trip',
];
$mock = Mockery::mock(AdministratorAbsenceService::class);
$mock->shouldReceive('submit')
->once()
->andReturn([
'message' => '1 day(s) saved as absent.',
'saved' => 1,
'dates' => ['2026-03-08'],
'status' => 200,
]);
$this->app->instance(AdministratorAbsenceService::class, $mock);
$response = $this->postJson('/api/administrator/absence', $payload);
$response->assertOk()
->assertJson([
'message' => '1 day(s) saved as absent.',
'saved' => 1,
'dates' => ['2026-03-08'],
]);
}
}
@@ -0,0 +1,131 @@
<?php
namespace Tests\Feature\Api\Administrator;
use App\Models\User;
use App\Services\Administrator\AdministratorEnrollmentQueryService;
use App\Services\Administrator\AdministratorEnrollmentStatusService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Mockery;
use Tests\TestCase;
class AdministratorEnrollmentControllerTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function test_guest_cannot_update_enrollment_statuses(): void
{
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
10 => 'enrolled',
],
]);
$response->assertStatus(401);
}
public function test_can_fetch_enrollment_withdrawal_data(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$mock = Mockery::mock(AdministratorEnrollmentQueryService::class);
$mock->shouldReceive('enrollmentWithdrawalData')
->once()
->with('2025-2026')
->andReturn([
'students' => [],
'classes' => [],
'semester' => 'Fall',
'school_year' => '2025-2026',
'schoolYears' => ['2025-2026'],
]);
$this->app->instance(AdministratorEnrollmentQueryService::class, $mock);
$response = $this->getJson('/api/administrator/enrollment-withdrawal?schoolYear=2025-2026');
$response->assertOk()
->assertJson([
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
}
public function test_can_fetch_new_students(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$mock = Mockery::mock(AdministratorEnrollmentQueryService::class);
$mock->shouldReceive('newStudentsData')
->once()
->andReturn([
'new_students' => [],
'total_new' => 0,
]);
$this->app->instance(AdministratorEnrollmentQueryService::class, $mock);
$response = $this->getJson('/api/administrator/enrollment-withdrawal/new-students');
$response->assertOk()
->assertJson([
'new_students' => [],
'total_new' => 0,
]);
}
public function test_update_statuses_requires_valid_status_values(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
10 => 'bad-status',
],
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['enrollment_status.10']);
}
public function test_update_statuses_calls_service(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'enrollment_status' => [
10 => 'enrolled',
11 => 'payment pending',
],
];
$mock = Mockery::mock(AdministratorEnrollmentStatusService::class);
$mock->shouldReceive('updateStatuses')
->once()
->with($payload['enrollment_status'], $user->id)
->andReturn([
'message' => 'Enrollment statuses updated and notifications sent.',
'status' => 200,
]);
$this->app->instance(AdministratorEnrollmentStatusService::class, $mock);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', $payload);
$response->assertOk()
->assertJson([
'message' => 'Enrollment statuses updated and notifications sent.',
]);
}
}
@@ -0,0 +1,227 @@
<?php
namespace Tests\Feature\Api\V1\Admin;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\Enrollment;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AdministratorEnrollmentQueryApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Seed config expected by AdministratorSharedService
Configuration::unguard();
Configuration::query()->create([
'config_key' => 'school_year',
'config_value' => '2025-2026',
]);
Configuration::query()->create([
'config_key' => 'semester',
'config_value' => 'Fall',
]);
}
public function test_enrollment_withdrawal_endpoint_returns_students_classes_and_years(): void
{
$admin = User::factory()->create();
Sanctum::actingAs($admin);
$parentOne = User::factory()->create([
'firstname' => 'Parent',
'lastname' => 'One',
]);
$parentTwo = User::factory()->create([
'firstname' => 'Parent',
'lastname' => 'Two',
]);
$studentOne = Student::query()->create([
'id' => 1,
'parent_id' => $parentOne->id,
'firstname' => 'Ali',
'lastname' => 'Ben',
'is_new' => 1,
'registration_date' => '2025-09-10 08:00:00',
'admission_status' => 'accepted',
]);
$studentTwo = Student::query()->create([
'id' => 2,
'parent_id' => $parentTwo->id,
'firstname' => 'Sara',
'lastname' => 'Ali',
'is_new' => 0,
'registration_date' => '2024-09-10 08:00:00',
'admission_status' => 'accepted',
]);
ClassSection::unguard();
ClassSection::query()->create([
'class_section_id' => 10,
'class_section_name' => 'Grade 5A',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
StudentClass::unguard();
StudentClass::query()->create([
'student_id' => $studentOne->id,
'class_section_id' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
Enrollment::unguard();
Enrollment::query()->create([
'student_id' => $studentOne->id,
'parent_id' => $parentOne->id,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'is_withdrawn' => 0,
'enrollment_date' => now()->toDateString(),
]);
Enrollment::query()->create([
'student_id' => $studentTwo->id,
'parent_id' => $parentTwo->id,
'school_year' => '2024-2025',
'semester' => 'Fall',
'enrollment_status' => 'withdrawn',
'admission_status' => 'accepted',
'is_withdrawn' => 1,
'enrollment_date' => now()->subYear()->toDateString(),
]);
$response = $this->getJson('/api/administrator/enrollment-withdrawal?schoolYear=2025-2026');
$response->assertOk()
->assertJson([
'semester' => 'Fall',
'school_year' => '2025-2026',
])
->assertJsonCount(2, 'students')
->assertJsonCount(1, 'classes');
$students = collect($response->json('students'));
$first = $students->firstWhere('student_id', 1);
$second = $students->firstWhere('student_id', 2);
$this->assertSame('Yes', $first['new_student']);
$this->assertSame('Grade 5A', $first['class_section']);
$this->assertSame('enrolled', $first['enrollment_status']);
$this->assertSame('2025-09-10', $first['registration_date_order']);
$this->assertSame('No', $second['new_student']);
$this->assertSame('Yes', $second['removed_previous_year']);
}
public function test_new_students_endpoint_returns_transformed_rows(): void
{
$admin = User::factory()->create();
Sanctum::actingAs($admin);
$parent = User::factory()->create([
'firstname' => 'Parent',
'lastname' => 'House',
]);
$studentOne = Student::query()->create([
'id' => 9,
'parent_id' => $parent->id,
'firstname' => 'Adam',
'lastname' => 'Y',
'is_new' => 1,
'registration_date' => '2025-10-01 10:00:00',
'admission_status' => 'accepted',
]);
$studentTwo = Student::query()->create([
'id' => 10,
'parent_id' => $parent->id,
'firstname' => 'Eve',
'lastname' => 'Z',
'is_new' => 0,
'registration_date' => null,
'admission_status' => 'accepted',
]);
ClassSection::unguard();
ClassSection::query()->create([
'class_section_id' => 20,
'class_section_name' => 'Grade 1A',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
StudentClass::unguard();
StudentClass::query()->create([
'student_id' => $studentOne->id,
'class_section_id' => 20,
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
Enrollment::unguard();
Enrollment::query()->create([
'student_id' => $studentOne->id,
'parent_id' => $parent->id,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'is_withdrawn' => 0,
'enrollment_date' => now()->toDateString(),
]);
Enrollment::query()->create([
'student_id' => $studentTwo->id,
'parent_id' => $parent->id,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'waitlist',
'admission_status' => 'pending',
'is_withdrawn' => 0,
'enrollment_date' => now()->toDateString(),
]);
$response = $this->getJson('/api/administrator/enrollment-withdrawal/new-students');
$response->assertOk()
->assertJson([
'total_new' => 2,
])
->assertJsonCount(2, 'new_students');
$students = collect($response->json('new_students'));
$first = $students->firstWhere('id', 9);
$second = $students->firstWhere('id', 10);
$this->assertSame('Yes', $first['new_student']);
$this->assertSame('Grade 1A', $first['class_section']);
$this->assertSame('enrolled', $first['enrollment_status']);
$this->assertSame('contact_9', $first['modalIdContact']);
$this->assertSame('2025-10-01', $first['registration_date']);
$this->assertSame('No', $second['new_student']);
$this->assertSame('Class not Assigned', $second['class_section']);
$this->assertSame('waitlist', $second['enrollment_status']);
}
}
@@ -0,0 +1,282 @@
<?php
namespace Tests\Feature\Api\Administrator;
use App\Models\Configuration;
use App\Models\Enrollment;
use App\Models\Invoice;
use App\Models\RefundModel as Refund;
use App\Models\Student;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AdministratorEnrollmentUpdateStatusesApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Configuration::unguard();
Configuration::query()->create([
'config_key' => 'school_year',
'config_value' => '2025-2026',
]);
Configuration::query()->create([
'config_key' => 'semester',
'config_value' => 'Fall',
]);
}
public function test_update_statuses_requires_enrollment_status_payload(): void
{
$admin = User::factory()->create();
Sanctum::actingAs($admin);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', []);
$response->assertStatus(422)
->assertJsonValidationErrors(['enrollment_status']);
}
public function test_update_statuses_rejects_invalid_status_value(): void
{
$admin = User::factory()->create();
Sanctum::actingAs($admin);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
10 => 'not-valid',
],
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['enrollment_status.10']);
}
public function test_update_statuses_creates_new_enrollment_row(): void
{
Event::fake();
$admin = User::factory()->create(['id' => 900]);
Sanctum::actingAs($admin);
$parent = User::factory()->create([
'id' => 100,
'firstname' => 'Parent',
'lastname' => 'One',
'email' => 'parent1@test.com',
]);
Student::unguard();
Student::query()->create([
'id' => 10,
'parent_id' => $parent->id,
'firstname' => 'Ali',
'lastname' => 'Ben',
'admission_status' => 'pending',
]);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
10 => 'enrolled',
],
]);
$response->assertOk()
->assertJson([
'message' => 'Enrollment statuses updated and notifications sent.',
]);
$this->assertDatabaseHas('enrollments', [
'student_id' => 10,
'parent_id' => 100,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'is_withdrawn' => 0,
]);
}
public function test_update_statuses_updates_existing_enrollment_row(): void
{
Event::fake();
$admin = User::factory()->create(['id' => 901]);
Sanctum::actingAs($admin);
$parent = User::factory()->create([
'id' => 101,
'firstname' => 'Parent',
'lastname' => 'Two',
'email' => 'parent2@test.com',
]);
Student::unguard();
Student::query()->create([
'id' => 11,
'parent_id' => $parent->id,
'firstname' => 'Sara',
'lastname' => 'Ali',
'admission_status' => 'pending',
]);
Enrollment::unguard();
Enrollment::query()->create([
'student_id' => 11,
'parent_id' => 101,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'payment pending',
'admission_status' => 'accepted',
'is_withdrawn' => 0,
'enrollment_date' => now()->toDateString(),
]);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
11 => 'withdrawn',
],
]);
$response->assertOk();
$this->assertDatabaseHas('enrollments', [
'student_id' => 11,
'parent_id' => 101,
'school_year' => '2025-2026',
'enrollment_status' => 'withdrawn',
'admission_status' => 'pending',
]);
}
public function test_update_statuses_creates_or_updates_refund_when_status_is_refund_pending(): void
{
Event::fake();
$admin = User::factory()->create(['id' => 902]);
Sanctum::actingAs($admin);
$parent = User::factory()->create([
'id' => 102,
'firstname' => 'Parent',
'lastname' => 'Three',
'email' => 'parent3@test.com',
]);
Student::unguard();
Student::query()->create([
'id' => 12,
'parent_id' => $parent->id,
'firstname' => 'Mina',
'lastname' => 'K',
'admission_status' => 'accepted',
]);
Enrollment::unguard();
Enrollment::query()->create([
'student_id' => 12,
'parent_id' => 102,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'is_withdrawn' => 0,
'enrollment_date' => now()->toDateString(),
]);
Invoice::unguard();
$invoice = Invoice::query()->create([
'parent_id' => 102,
'school_year' => '2025-2026',
'balance' => 125.00,
'amount_due' => 125.00,
'total_amount' => 125.00,
'due_date' => now()->addDays(10)->toDateString(),
]);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
12 => 'refund pending',
],
]);
$response->assertOk();
$this->assertDatabaseHas('enrollments', [
'student_id' => 12,
'parent_id' => 102,
'school_year' => '2025-2026',
'enrollment_status' => 'refund pending',
]);
$this->assertDatabaseHas('refunds', [
'parent_id' => 102,
'invoice_id' => $invoice->id,
'status' => 'Pending',
'updated_by' => 902,
]);
$refund = Refund::query()->where('invoice_id', $invoice->id)->first();
$this->assertNotNull($refund);
$this->assertGreaterThanOrEqual(0, (float) $refund->refund_amount);
}
public function test_update_statuses_returns_partial_success_when_some_students_are_invalid_for_processing(): void
{
Event::fake();
$admin = User::factory()->create(['id' => 903]);
Sanctum::actingAs($admin);
$parent = User::factory()->create([
'id' => 103,
'firstname' => 'Parent',
'lastname' => 'Four',
'email' => 'parent4@test.com',
]);
Student::unguard();
Student::query()->create([
'id' => 13,
'parent_id' => $parent->id,
'firstname' => 'Adam',
'lastname' => 'Q',
'admission_status' => 'pending',
]);
Student::query()->create([
'id' => 14,
'parent_id' => null,
'firstname' => 'No',
'lastname' => 'Parent',
'admission_status' => 'pending',
]);
$response = $this->postJson('/api/administrator/enrollment-withdrawal/update-statuses', [
'enrollment_status' => [
13 => 'enrolled',
14 => 'enrolled',
],
]);
$response->assertStatus(207);
$this->assertDatabaseHas('enrollments', [
'student_id' => 13,
'parent_id' => 103,
'school_year' => '2025-2026',
'enrollment_status' => 'enrolled',
]);
$this->assertDatabaseMissing('enrollments', [
'student_id' => 14,
'school_year' => '2025-2026',
]);
}
}
@@ -0,0 +1,160 @@
<?php
namespace Tests\Feature\Api\Administrator;
use App\Models\User;
use App\Services\Administrator\AdminNotificationSubjectService;
use App\Services\Administrator\AdminPrintRecipientService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Mockery;
use Tests\TestCase;
class AdministratorNotificationControllerTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function test_can_get_alerts_data(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$mock = Mockery::mock(AdminNotificationSubjectService::class);
$mock->shouldReceive('alertsData')
->once()
->andReturn([
'admins' => [],
'subjects' => [
'academics' => 'Academics',
],
'assignedSubjects' => [],
'tableReady' => true,
]);
$this->app->instance(AdminNotificationSubjectService::class, $mock);
$response = $this->getJson('/api/administrator/notifications/alerts');
$response->assertOk()
->assertJson([
'admins' => [],
'assignedSubjects' => [],
'tableReady' => true,
]);
}
public function test_save_alerts_requires_subjects_payload(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->postJson('/api/administrator/notifications/alerts', []);
$response->assertStatus(422)
->assertJsonValidationErrors(['subjects']);
}
public function test_save_alerts_calls_subject_service(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'subjects' => [
1 => ['academics', 'finance'],
],
];
$mock = Mockery::mock(AdminNotificationSubjectService::class);
$mock->shouldReceive('save')
->once()
->with($payload['subjects'])
->andReturn([
'message' => 'Notification subjects updated.',
'status' => 200,
]);
$this->app->instance(AdminNotificationSubjectService::class, $mock);
$response = $this->postJson('/api/administrator/notifications/alerts', $payload);
$response->assertOk()
->assertJson([
'message' => 'Notification subjects updated.',
]);
}
public function test_can_get_print_recipients_data(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$mock = Mockery::mock(AdminPrintRecipientService::class);
$mock->shouldReceive('data')
->once()
->andReturn([
'admins' => [],
'assigned' => [],
'tableReady' => true,
]);
$this->app->instance(AdminPrintRecipientService::class, $mock);
$response = $this->getJson('/api/administrator/notifications/print-recipients');
$response->assertOk()
->assertJson([
'admins' => [],
'assigned' => [],
'tableReady' => true,
]);
}
public function test_save_print_recipients_requires_notify_payload(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->postJson('/api/administrator/notifications/print-recipients', []);
$response->assertStatus(422)
->assertJsonValidationErrors(['notify']);
}
public function test_save_print_recipients_calls_service(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'notify' => [
1 => 1,
2 => 1,
],
];
$mock = Mockery::mock(AdminPrintRecipientService::class);
$mock->shouldReceive('save')
->once()
->with($payload['notify'])
->andReturn([
'message' => 'Print notification recipients updated.',
'status' => 200,
]);
$this->app->instance(AdminPrintRecipientService::class, $mock);
$response = $this->postJson('/api/administrator/notifications/print-recipients', $payload);
$response->assertOk()
->assertJson([
'message' => 'Print notification recipients updated.',
]);
}
}
@@ -0,0 +1,108 @@
<?php
namespace Tests\Feature\Api\Administrator;
use App\Models\User;
use App\Services\Administrator\TeacherSubmissionNotificationService;
use App\Services\Administrator\TeacherSubmissionReportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Mockery;
use Tests\TestCase;
class AdministratorTeacherSubmissionControllerTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function test_can_get_teacher_submission_report(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$mock = Mockery::mock(TeacherSubmissionReportService::class);
$mock->shouldReceive('report')
->once()
->andReturn([
'rows' => [],
'semester' => 'Fall',
'schoolYear' => '2025-2026',
'notificationHistory' => [],
'summary' => [
'total_items' => 0,
'missing_items' => 0,
'submitted_items' => 0,
'submission_percentage' => 100,
],
]);
$this->app->instance(TeacherSubmissionReportService::class, $mock);
$response = $this->getJson('/api/administrator/teacher-submissions');
$response->assertOk()
->assertJson([
'rows' => [],
'semester' => 'Fall',
'schoolYear' => '2025-2026',
]);
}
public function test_notify_requires_notify_payload(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->postJson('/api/administrator/teacher-submissions/notify', [
'missing_items' => [],
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['notify']);
}
public function test_notify_calls_service(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$payload = [
'notify' => [
1 => [
5 => 1,
],
],
'missing_items' => [
1 => [
5 => base64_encode(json_encode(['midterm scores'])),
],
],
];
$mock = Mockery::mock(TeacherSubmissionNotificationService::class);
$mock->shouldReceive('send')
->once()
->andReturn([
'message' => '1 reminder sent',
'sent' => 1,
'failed' => 0,
'status' => 200,
]);
$this->app->instance(TeacherSubmissionNotificationService::class, $mock);
$response = $this->postJson('/api/administrator/teacher-submissions/notify', $payload);
$response->assertOk()
->assertJson([
'message' => '1 reminder sent',
'sent' => 1,
'failed' => 0,
]);
}
}
@@ -0,0 +1,170 @@
<?php
namespace Tests\Feature\Api\Administrator;
use App\Models\Configuration;
use App\Models\TeacherSubmissionNotificationHistory;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AdministratorTeacherSubmissionNotifyApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Configuration::unguard();
Configuration::query()->create([
'config_key' => 'school_year',
'config_value' => '2025-2026',
]);
Configuration::query()->create([
'config_key' => 'semester',
'config_value' => 'Fall',
]);
}
public function test_notify_endpoint_requires_notify_payload(): void
{
$admin = User::factory()->create();
Sanctum::actingAs($admin);
$response = $this->postJson('/api/administrator/teacher-submissions/notify', [
'missing_items' => [],
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['notify']);
}
public function test_notify_endpoint_sends_email_and_creates_history_row(): void
{
Mail::fake();
$admin = User::factory()->create([
'id' => 900,
'firstname' => 'Admin',
'lastname' => 'One',
'email' => 'admin@test.com',
]);
Sanctum::actingAs($admin);
DB::table('users')->insert([
[
'id' => 100,
'firstname' => 'John',
'lastname' => 'Teacher',
'email' => 'teacher@test.com',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 10,
'class_section_name' => 'Grade 5A',
],
]);
$payload = [
'notify' => [
10 => [
100 => 1,
],
],
'missing_items' => [
10 => [
100 => base64_encode(json_encode([
'midterm scores',
'attendance',
])),
],
],
];
$response = $this->postJson('/api/administrator/teacher-submissions/notify', $payload);
$response->assertOk()
->assertJson([
'sent' => 1,
'failed' => 0,
]);
Mail::assertSentCount(1);
$this->assertDatabaseHas('teacher_submission_notification_history', [
'teacher_id' => 100,
'class_section_id' => 10,
'admin_id' => 900,
'notification_category' => 'teacher_submissions',
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$history = TeacherSubmissionNotificationHistory::query()->latest('id')->first();
$this->assertNotNull($history);
$this->assertStringContainsString('midterm scores and attendance', strtolower((string) $history->message));
}
public function test_notify_endpoint_marks_failed_when_teacher_email_is_invalid(): void
{
Mail::fake();
$admin = User::factory()->create([
'id' => 901,
'firstname' => 'Admin',
'lastname' => 'Two',
]);
Sanctum::actingAs($admin);
DB::table('users')->insert([
[
'id' => 101,
'firstname' => 'Bad',
'lastname' => 'Email',
'email' => 'not-an-email',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 11,
'class_section_name' => 'Grade 6A',
],
]);
$payload = [
'notify' => [
11 => [
101 => 1,
],
],
];
$response = $this->postJson('/api/administrator/teacher-submissions/notify', $payload);
$response->assertStatus(207)
->assertJson([
'sent' => 0,
'failed' => 1,
]);
Mail::assertNothingSent();
$this->assertDatabaseHas('teacher_submission_notification_history', [
'teacher_id' => 101,
'class_section_id' => 11,
'admin_id' => 901,
'notification_category' => 'teacher_submissions',
'status' => 'failed',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
}
}
@@ -0,0 +1,292 @@
<?php
namespace Tests\Feature\Api\Administrator;
use App\Models\AttendanceDay;
use App\Models\Configuration;
use App\Models\ScoreComment;
use App\Models\SemesterScore;
use App\Models\TeacherSubmissionNotificationHistory;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AdministratorTeacherSubmissionReportApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Configuration::unguard();
Configuration::query()->create([
'config_key' => 'school_year',
'config_value' => '2025-2026',
]);
Configuration::query()->create([
'config_key' => 'semester',
'config_value' => 'Fall',
]);
}
public function test_teacher_submission_report_endpoint_returns_rows_summary_and_history(): void
{
$admin = User::factory()->create(['id' => 900, 'firstname' => 'Admin', 'lastname' => 'One']);
Sanctum::actingAs($admin);
DB::table('users')->insert([
[
'id' => 100,
'firstname' => 'Main',
'lastname' => 'Teacher',
'email' => 'main@test.com',
],
[
'id' => 101,
'firstname' => 'TA',
'lastname' => 'Teacher',
'email' => 'ta@test.com',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 10,
'class_section_name' => 'Grade 5A',
],
]);
DB::table('teacher_class')->insert([
[
'class_section_id' => 10,
'teacher_id' => 100,
'position' => 'main',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
[
'class_section_id' => 10,
'teacher_id' => 101,
'position' => 'ta',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
DB::table('student_class')->insert([
[
'student_id' => 1,
'class_section_id' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
],
[
'student_id' => 2,
'class_section_id' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
SemesterScore::unguard();
SemesterScore::query()->create([
'student_id' => 1,
'class_section_id' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
'midterm_exam_score' => '88',
'participation_score' => '10',
]);
SemesterScore::query()->create([
'student_id' => 2,
'class_section_id' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
'midterm_exam_score' => '',
'participation_score' => '9',
]);
ScoreComment::unguard();
ScoreComment::query()->create([
'student_id' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
'score_type' => 'midterm',
'comment' => 'Good progress',
]);
ScoreComment::query()->create([
'student_id' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
'score_type' => 'ptap',
'comment' => 'Great effort',
]);
AttendanceDay::unguard();
AttendanceDay::query()->create([
'class_section_id' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
'date' => now()->format('Y-m-d'),
'status' => 'submitted',
]);
TeacherSubmissionNotificationHistory::unguard();
TeacherSubmissionNotificationHistory::query()->create([
'teacher_id' => 100,
'class_section_id' => 10,
'admin_id' => 900,
'notification_category' => 'teacher_submissions',
'message' => 'Reminder sent',
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
'sent_at' => now(),
]);
$response = $this->getJson('/api/administrator/teacher-submissions');
$response->assertOk()
->assertJson([
'semester' => 'Fall',
'schoolYear' => '2025-2026',
])
->assertJsonCount(1, 'rows');
$row = $response->json('rows.0');
$this->assertSame('Grade 5A', $row['class_section']);
$this->assertSame(10, $row['class_section_id']);
$this->assertCount(2, $row['teachers']);
$this->assertSame('Main: Main Teacher', $row['teachers'][0]['label']);
$this->assertSame('TA: TA Teacher', $row['teachers'][1]['label']);
$this->assertSame('Missing', $row['midterm_score_status']['label']);
$this->assertSame('1/2', $row['midterm_score_status']['detail']);
$this->assertSame('Missing', $row['midterm_comment_status']['label']);
$this->assertSame('1/2', $row['midterm_comment_status']['detail']);
$this->assertSame('Submitted', $row['participation_status']['label']);
$this->assertSame('2/2', $row['participation_status']['detail']);
$this->assertSame('Missing', $row['ptap_comment_status']['label']);
$this->assertSame('1/2', $row['ptap_comment_status']['detail']);
$this->assertSame('Submitted', $row['attendance_status']['label']);
$this->assertSame([
'midterm scores',
'midterm comments',
'PTAP comments',
], $row['missing_items']);
$summary = $response->json('summary');
$this->assertSame(5, $summary['total_items']);
$this->assertSame(3, $summary['missing_items']);
$this->assertSame(2, $summary['submitted_items']);
$this->assertSame(40, $summary['submission_percentage']);
$history = $response->json('notificationHistory');
$this->assertSame('Admin One', $history['10']['100'][0]['admin_name']);
}
public function test_teacher_submission_report_returns_no_students_status_for_empty_section(): void
{
$admin = User::factory()->create();
Sanctum::actingAs($admin);
DB::table('users')->insert([
[
'id' => 200,
'firstname' => 'Solo',
'lastname' => 'Teacher',
'email' => 'solo@test.com',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 11,
'class_section_name' => 'Grade 6A',
],
]);
DB::table('teacher_class')->insert([
[
'class_section_id' => 11,
'teacher_id' => 200,
'position' => 'main',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
$response = $this->getJson('/api/administrator/teacher-submissions');
$response->assertOk()
->assertJsonCount(1, 'rows');
$row = $response->json('rows.0');
$this->assertSame('No students', $row['midterm_score_status']['label']);
$this->assertSame('No students', $row['midterm_comment_status']['label']);
$this->assertSame('No students', $row['participation_status']['label']);
$this->assertSame('No students', $row['ptap_comment_status']['label']);
$this->assertSame([], $row['missing_items']);
$this->assertSame(0, $row['student_count']);
}
public function test_teacher_submission_report_limits_history_to_three_entries(): void
{
$admin = User::factory()->create(['id' => 901, 'firstname' => 'Admin', 'lastname' => 'Two']);
Sanctum::actingAs($admin);
DB::table('users')->insert([
[
'id' => 300,
'firstname' => 'Hist',
'lastname' => 'Teacher',
'email' => 'hist@test.com',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 12,
'class_section_name' => 'Grade 7A',
],
]);
DB::table('teacher_class')->insert([
[
'class_section_id' => 12,
'teacher_id' => 300,
'position' => 'main',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
TeacherSubmissionNotificationHistory::unguard();
for ($i = 0; $i < 4; $i++) {
TeacherSubmissionNotificationHistory::query()->create([
'teacher_id' => 300,
'class_section_id' => 12,
'admin_id' => 901,
'notification_category' => 'teacher_submissions',
'message' => 'Reminder ' . $i,
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
'sent_at' => now()->subMinutes($i),
]);
}
$response = $this->getJson('/api/administrator/teacher-submissions');
$response->assertOk();
$history = $response->json('notificationHistory.12.300');
$this->assertCount(3, $history);
}
}