reconstruction of the project
This commit is contained in:
@@ -1,169 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminNotificationApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function createAdmin(): User
|
||||
{
|
||||
return User::factory()->create([
|
||||
'role' => 'administrator',
|
||||
'firstname' => 'Super',
|
||||
'lastname' => 'Admin',
|
||||
'email' => 'superadmin@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function seedRoleDataForAdmins(): array
|
||||
{
|
||||
// roles + user_roles used by AdminNotificationService::fetchAdminNotificationUsers()
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'administrator'],
|
||||
['id' => 2, 'name' => 'teacher'],
|
||||
]);
|
||||
|
||||
$adminA = User::factory()->create([
|
||||
'firstname' => 'Nour',
|
||||
'lastname' => 'Ali',
|
||||
'email' => 'nour@example.com',
|
||||
'role' => 'administrator',
|
||||
]);
|
||||
|
||||
$adminB = User::factory()->create([
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Omar',
|
||||
'email' => 'sara@example.com',
|
||||
'role' => 'administrator',
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'email' => 'teacher@example.com',
|
||||
'role' => 'teacher',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => $adminA->id, 'role_id' => 1, 'deleted_at' => null],
|
||||
['user_id' => $adminB->id, 'role_id' => 1, 'deleted_at' => null],
|
||||
['user_id' => $teacher->id, 'role_id' => 2, 'deleted_at' => null],
|
||||
]);
|
||||
|
||||
return [$adminA, $adminB, $teacher];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_get_notification_alerts_payload(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
$this->seedRoleDataForAdmins();
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->getJson('/api/admin/notifications/alerts');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'admins',
|
||||
'subjects',
|
||||
'assigned_subjects',
|
||||
'table_ready',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_save_notification_subjects(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
[$adminA, $adminB] = $this->seedRoleDataForAdmins();
|
||||
|
||||
$payload = [
|
||||
'subjects' => [
|
||||
$adminA->id => [
|
||||
'finance' => true,
|
||||
'attendance' => true,
|
||||
],
|
||||
$adminB->id => [
|
||||
'general',
|
||||
'events',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->postJson('/api/admin/notifications/subjects', $payload);
|
||||
|
||||
// If table is missing in your migrations, service returns 422 (ok=false)
|
||||
if ($response->status() === 422) {
|
||||
$response->assertJson([
|
||||
'ok' => false,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('admin_notification_subjects', [
|
||||
'admin_id' => $adminA->id,
|
||||
'subject' => 'finance',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_get_print_recipients_payload(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
$this->seedRoleDataForAdmins();
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->getJson('/api/admin/notifications/print-recipients');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'admins',
|
||||
'assigned',
|
||||
'table_ready',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_save_print_recipients(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
[$adminA, $adminB] = $this->seedRoleDataForAdmins();
|
||||
|
||||
$payload = [
|
||||
'notify' => [
|
||||
(string)$adminA->id => true,
|
||||
(string)$adminB->id => true,
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->postJson('/api/admin/notifications/print-recipients', $payload);
|
||||
|
||||
if ($response->status() === 422) {
|
||||
$response->assertJson(['ok' => false]);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('admin_notification_subjects', [
|
||||
'admin_id' => $adminA->id,
|
||||
'subject' => 'print_requests',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminProgressApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createProgressTestTablesIfMissing();
|
||||
config()->set('progress.status_options', [
|
||||
'draft' => 'Draft',
|
||||
'submitted' => 'Submitted',
|
||||
'reviewed' => 'Reviewed',
|
||||
'approved' => 'Approved',
|
||||
'rejected' => 'Rejected',
|
||||
]);
|
||||
config()->set('progress.subject_sections', [
|
||||
'quran' => ['Reading', 'Memorization'],
|
||||
'arabic' => ['Reading', 'Writing'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_lists_grouped_progress_reports(): void
|
||||
{
|
||||
$sectionId = $this->insertClassSection('Grade 5 - A');
|
||||
$this->insertStudentClass($sectionId);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/admin/progress');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'*' => [
|
||||
'week_start',
|
||||
'week_end',
|
||||
'class_section_id',
|
||||
'class_section_name',
|
||||
'reports',
|
||||
],
|
||||
],
|
||||
'meta' => [
|
||||
'filters',
|
||||
'class_sections',
|
||||
'status_options',
|
||||
'subject_sections',
|
||||
],
|
||||
]);
|
||||
|
||||
$data = $response->json('data');
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertEquals('2026-02-09', $data[0]['week_start']);
|
||||
$this->assertEquals($sectionId, $data[0]['class_section_id']);
|
||||
$this->assertArrayHasKey('quran', $data[0]['reports']);
|
||||
$this->assertArrayHasKey('arabic', $data[0]['reports']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_filters_progress_reports_by_status_and_class_section(): void
|
||||
{
|
||||
$sectionA = $this->insertClassSection('Grade 4 - A');
|
||||
$sectionB = $this->insertClassSection('Grade 6 - B');
|
||||
|
||||
$this->insertStudentClass($sectionA);
|
||||
$this->insertStudentClass($sectionB);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionA,
|
||||
'subject' => 'quran',
|
||||
'status' => 'approved',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionB,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'draft',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/admin/progress?status=approved&class_section_id=' . $sectionA);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$data = $response->json('data');
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertEquals($sectionA, $data[0]['class_section_id']);
|
||||
$this->assertArrayHasKey('quran', $data[0]['reports']);
|
||||
$this->assertArrayNotHasKey('arabic', $data[0]['reports']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_shows_progress_report_with_weekly_reports_and_attachments(): void
|
||||
{
|
||||
$sectionId = $this->insertClassSection('Grade 5 - A');
|
||||
$report1 = $this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'flags_json' => json_encode(['needs_followup' => true]),
|
||||
]);
|
||||
|
||||
$report2 = $this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$this->insertAttachment($report1, [
|
||||
'original_name' => 'quran-notes.pdf',
|
||||
'file_path' => 'uploads/progress/quran-notes.pdf',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/admin/progress/reports/' . $report1);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'report',
|
||||
'weekly_reports',
|
||||
'subject_sections',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals($report1, $response->json('data.report.id'));
|
||||
$this->assertEquals('Submitted', $response->json('data.report.status_label'));
|
||||
$this->assertTrue($response->json('data.report.flags.needs_followup'));
|
||||
|
||||
$weekly = $response->json('data.weekly_reports');
|
||||
$this->assertCount(2, $weekly);
|
||||
|
||||
$quranReport = collect($weekly)->firstWhere('subject', 'quran');
|
||||
$this->assertNotNull($quranReport);
|
||||
$this->assertNotEmpty($quranReport['attachments']);
|
||||
$this->assertEquals('quran-notes.pdf', $quranReport['attachments'][0]['name']);
|
||||
|
||||
$arabicReport = collect($weekly)->firstWhere('subject', 'arabic');
|
||||
$this->assertNotNull($arabicReport);
|
||||
$this->assertIsArray($arabicReport['attachments']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_for_missing_progress_report(): void
|
||||
{
|
||||
$this->getJson('/api/admin/progress/reports/999999')
|
||||
->assertStatus(404)
|
||||
->assertJson([
|
||||
'message' => 'Progress report not found.',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_downloads_legacy_attachment_from_report_attachment_path(): void
|
||||
{
|
||||
File::ensureDirectoryExists(storage_path('app/uploads/progress'));
|
||||
$fullPath = storage_path('app/uploads/progress/legacy-file.txt');
|
||||
File::put($fullPath, 'legacy attachment content');
|
||||
|
||||
$reportId = $this->insertProgressReport([
|
||||
'class_section_id' => 1,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'attachment_path' => 'writable/uploads/progress/legacy-file.txt',
|
||||
]);
|
||||
|
||||
$response = $this->get('/api/admin/progress/reports/' . $reportId . '/attachment');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('content-disposition');
|
||||
$this->assertStringContainsString('legacy-file.txt', $response->headers->get('content-disposition', ''));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_when_legacy_attachment_is_missing(): void
|
||||
{
|
||||
$reportId = $this->insertProgressReport([
|
||||
'class_section_id' => 1,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'attachment_path' => 'writable/uploads/progress/missing.txt',
|
||||
]);
|
||||
|
||||
$this->get('/api/admin/progress/reports/' . $reportId . '/attachment')
|
||||
->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_downloads_normalized_attachment_by_attachment_id(): void
|
||||
{
|
||||
File::ensureDirectoryExists(storage_path('app/uploads/progress'));
|
||||
$fullPath = storage_path('app/uploads/progress/normalized-file.txt');
|
||||
File::put($fullPath, 'normalized attachment content');
|
||||
|
||||
$reportId = $this->insertProgressReport([
|
||||
'class_section_id' => 1,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'approved',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$attachmentId = $this->insertAttachment($reportId, [
|
||||
'original_name' => 'Teacher Notes.txt',
|
||||
'file_path' => 'writable/uploads/progress/normalized-file.txt',
|
||||
]);
|
||||
|
||||
$response = $this->get('/api/admin/progress/attachments/' . $attachmentId);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertStringContainsString('Teacher Notes.txt', $response->headers->get('content-disposition', ''));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_for_missing_normalized_attachment(): void
|
||||
{
|
||||
$this->get('/api/admin/progress/attachments/999999')
|
||||
->assertStatus(404);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private function createProgressTestTablesIfMissing(): void
|
||||
{
|
||||
if (!Schema::hasTable('classSection')) {
|
||||
Schema::create('classSection', function (Blueprint $table) {
|
||||
$table->increments('class_section_id');
|
||||
$table->string('class_section_name')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('student_class')) {
|
||||
Schema::create('student_class', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('class_section_id');
|
||||
$table->unsignedInteger('student_id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('users')) {
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('firstname')->nullable();
|
||||
$table->string('lastname')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('password')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
} else {
|
||||
// Add columns if test app users table exists but lacks firstname/lastname
|
||||
if (!Schema::hasColumn('users', 'firstname')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('firstname')->nullable();
|
||||
});
|
||||
}
|
||||
if (!Schema::hasColumn('users', 'lastname')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('lastname')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('class_progress_reports')) {
|
||||
Schema::create('class_progress_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('class_section_id')->nullable();
|
||||
$table->unsignedBigInteger('teacher_id')->nullable();
|
||||
$table->string('subject')->nullable();
|
||||
$table->date('week_start')->nullable();
|
||||
$table->date('week_end')->nullable();
|
||||
$table->string('status')->nullable();
|
||||
$table->text('flags_json')->nullable();
|
||||
$table->string('attachment_path')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('class_progress_attachments')) {
|
||||
Schema::create('class_progress_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('report_id');
|
||||
$table->string('original_name')->nullable();
|
||||
$table->string('file_path')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function insertClassSection(string $name): int
|
||||
{
|
||||
return (int) \DB::table('classSection')->insertGetId([
|
||||
'class_section_name' => $name,
|
||||
], 'class_section_id');
|
||||
}
|
||||
|
||||
private function insertStudentClass(int $classSectionId): int
|
||||
{
|
||||
return (int) \DB::table('student_class')->insertGetId([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => rand(1000, 9999),
|
||||
]);
|
||||
}
|
||||
|
||||
private function insertProgressReport(array $override = []): int
|
||||
{
|
||||
$data = array_merge([
|
||||
'class_section_id' => 1,
|
||||
'teacher_id' => null,
|
||||
'subject' => 'quran',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'status' => 'draft',
|
||||
'flags_json' => null,
|
||||
'attachment_path' => null,
|
||||
'notes' => 'Test report',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
], $override);
|
||||
|
||||
return (int) \DB::table('class_progress_reports')->insertGetId($data);
|
||||
}
|
||||
|
||||
private function insertAttachment(int $reportId, array $override = []): int
|
||||
{
|
||||
$data = array_merge([
|
||||
'report_id' => $reportId,
|
||||
'original_name' => 'Attachment.pdf',
|
||||
'file_path' => 'writable/uploads/progress/attachment.pdf',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
], $override);
|
||||
|
||||
return (int) \DB::table('class_progress_attachments')->insertGetId($data);
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TeacherSubmissionApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function createAdmin(): User
|
||||
{
|
||||
$admin = User::factory()->create([
|
||||
'role' => 'administrator',
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'email' => 'admin@example.com',
|
||||
]);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
protected function seedSchoolConfig(): void
|
||||
{
|
||||
DB::table('configurations')->insert([
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_fetch_teacher_submission_report(): void
|
||||
{
|
||||
$admin = $this->createAdmin();
|
||||
$this->seedSchoolConfig();
|
||||
|
||||
// Minimal records to allow report query to run
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => 'Grade 5 - A',
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Fatima',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'teacher1@example.com',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => 1,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin, 'sanctum')
|
||||
->getJson('/api/admin/teacher-submissions/report');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'semester',
|
||||
'school_year',
|
||||
'rows',
|
||||
'notification_history',
|
||||
'summary' => [
|
||||
'total_items',
|
||||
'missing_items',
|
||||
'submitted_items',
|
||||
'submission_percentage',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_send_teacher_submission_notifications(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$admin = $this->createAdmin();
|
||||
$this->seedSchoolConfig();
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 2,
|
||||
'class_section_name' => 'Grade 6 - B',
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Amina',
|
||||
'lastname' => 'Smith',
|
||||
'email' => 'amina@example.com',
|
||||
]);
|
||||
|
||||
// optional history table insert compatibility depends on schema; no need to seed rows
|
||||
$payload = [
|
||||
'notify' => [
|
||||
2 => [
|
||||
$teacher->id => 1,
|
||||
],
|
||||
],
|
||||
'missing_items' => [
|
||||
2 => [
|
||||
$teacher->id => base64_encode(json_encode(['midterm scores', 'PTAP comments'])),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($admin, 'sanctum')
|
||||
->postJson('/api/admin/teacher-submissions/notifications', $payload);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'message',
|
||||
'sent',
|
||||
'failed',
|
||||
'results' => [
|
||||
'*' => [
|
||||
'teacher_id',
|
||||
'class_section_id',
|
||||
'teacher_name',
|
||||
'section_name',
|
||||
'email',
|
||||
'status',
|
||||
'missing_items',
|
||||
'error',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Mail::assertSentCount(1);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function teacher_submission_notifications_requires_notify_payload(): void
|
||||
{
|
||||
$admin = $this->createAdmin();
|
||||
|
||||
$response = $this->actingAs($admin, 'sanctum')
|
||||
->postJson('/api/admin/teacher-submissions/notifications', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['notify']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function non_admin_cannot_access_teacher_submission_endpoints(): void
|
||||
{
|
||||
$user = User::factory()->create(['role' => 'teacher']);
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->getJson('/api/admin/teacher-submissions/report')
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentApiControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
protected User $teacherMain;
|
||||
protected User $teacherAssistant;
|
||||
protected ClassSection $sectionA;
|
||||
protected ClassSection $sectionB;
|
||||
protected Student $studentOne;
|
||||
protected Student $studentTwo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->seedConfigurations();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->teacherMain = User::factory()->create([
|
||||
'firstname' => 'Main',
|
||||
'lastname' => 'Teacher',
|
||||
]);
|
||||
$this->teacherAssistant = User::factory()->create([
|
||||
'firstname' => 'Assistant',
|
||||
'lastname' => 'Teacher',
|
||||
]);
|
||||
|
||||
$this->sectionA = ClassSection::factory()->create([
|
||||
'section_name' => 'Grade 1 - A',
|
||||
]);
|
||||
|
||||
$this->sectionB = ClassSection::factory()->create([
|
||||
'section_name' => 'Grade 2 - B',
|
||||
]);
|
||||
|
||||
$this->studentOne = Student::factory()->create([
|
||||
'firstname' => 'Ali',
|
||||
'lastname' => 'Hassan',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'registration_grade' => 'Grade 1',
|
||||
'photo_consent' => 1,
|
||||
'tuition_paid' => 0,
|
||||
'school_id' => 'SCH-001',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$this->studentTwo = Student::factory()->create([
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Ibrahim',
|
||||
'age' => 9,
|
||||
'gender' => 'Female',
|
||||
'registration_grade' => 'Grade 2',
|
||||
'photo_consent' => 0,
|
||||
'tuition_paid' => 1,
|
||||
'school_id' => 'SCH-002',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function seedConfigurations(): void
|
||||
{
|
||||
Configuration::query()->create([
|
||||
'config_key' => 'semester',
|
||||
'config_value' => 'Fall',
|
||||
]);
|
||||
|
||||
Configuration::query()->create([
|
||||
'config_key' => 'school_year',
|
||||
'config_value' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_index_returns_assignment_overview(): void
|
||||
{
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherAssistant->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'ta',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
]);
|
||||
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->getJson('/api/assignments?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('message', 'Assignments retrieved successfully.')
|
||||
->assertJsonPath('data.schoolYear', '2025-2026')
|
||||
->assertJsonPath('data.selectedYear', '2025-2026')
|
||||
->assertJsonPath('data.selectedSemester', 'Fall')
|
||||
->assertJsonPath('data.semester', 'Fall')
|
||||
->assertJsonPath('data.current_school_year', '2025-2026')
|
||||
->assertJsonCount(1, 'data.classSections')
|
||||
->assertJsonPath('data.classSections.0.class_section_id', $this->sectionA->id)
|
||||
->assertJsonPath('data.classSections.0.class_section_name', 'Grade 1 - A')
|
||||
->assertJsonPath('data.classSections.0.main_teachers.0', 'Main Teacher')
|
||||
->assertJsonPath('data.classSections.0.teacher_assistants.0', 'Assistant Teacher')
|
||||
->assertJsonPath('data.classSections.0.students.0.firstname', 'Ali')
|
||||
->assertJsonPath('data.classSections.0.students.0.lastname', 'Hassan')
|
||||
->assertJsonPath('data.classSections.0.students.0.photo_consent', true)
|
||||
->assertJsonPath('data.classSections.0.students.0.tuition_paid', false)
|
||||
->assertJsonPath('data.classSections.0.description', 'Morning group');
|
||||
}
|
||||
|
||||
public function test_index_filters_by_school_year(): void
|
||||
{
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Included',
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherAssistant->id,
|
||||
'class_section_id' => $this->sectionB->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2024-2025',
|
||||
'description' => 'Excluded',
|
||||
]);
|
||||
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Included',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $this->studentTwo->id,
|
||||
'class_section_id' => $this->sectionB->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2024-2025',
|
||||
'description' => 'Excluded',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->getJson('/api/assignments?school_year=2025-2026');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonCount(1, 'data.classSections')
|
||||
->assertJsonPath('data.classSections.0.class_section_name', 'Grade 1 - A');
|
||||
}
|
||||
|
||||
public function test_index_sorts_sections_by_name(): void
|
||||
{
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionB->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'B group',
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherAssistant->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'A group',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->getJson('/api/assignments');
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$sections = $response->json('data.classSections');
|
||||
|
||||
$this->assertCount(2, $sections);
|
||||
$this->assertSame('Grade 1 - A', $sections[0]['class_section_name']);
|
||||
$this->assertSame('Grade 2 - B', $sections[1]['class_section_name']);
|
||||
}
|
||||
|
||||
public function test_index_excludes_inactive_students(): void
|
||||
{
|
||||
$inactiveStudent = Student::factory()->create([
|
||||
'firstname' => 'Hidden',
|
||||
'lastname' => 'Student',
|
||||
'is_active' => 0,
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
]);
|
||||
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $inactiveStudent->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Should not show',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->getJson('/api/assignments');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.classSections.0.students', []);
|
||||
}
|
||||
|
||||
public function test_store_creates_new_assignment(): void
|
||||
{
|
||||
$payload = [
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'New assignment',
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->postJson('/api/assignments', $payload);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('message', 'Assignment saved successfully.')
|
||||
->assertJsonPath('data.student_id', $this->studentOne->id)
|
||||
->assertJsonPath('data.class_section_id', $this->sectionA->id)
|
||||
->assertJsonPath('data.semester', 'Fall')
|
||||
->assertJsonPath('data.school_year', '2025-2026')
|
||||
->assertJsonPath('data.description', 'New assignment');
|
||||
|
||||
$this->assertDatabaseHas('student_class', [
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'New assignment',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_updates_existing_assignment_for_same_unique_keys(): void
|
||||
{
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Old description',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Updated description',
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->postJson('/api/assignments', $payload);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.description', 'Updated description');
|
||||
|
||||
$this->assertDatabaseHas('student_class', [
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Updated description',
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
StudentClass::query()
|
||||
->where('student_id', $this->studentOne->id)
|
||||
->where('class_section_id', $this->sectionA->id)
|
||||
->where('semester', 'Fall')
|
||||
->where('school_year', '2025-2026')
|
||||
->count()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_store_validates_required_fields(): void
|
||||
{
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->postJson('/api/assignments', []);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors([
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_validates_foreign_keys(): void
|
||||
{
|
||||
$payload = [
|
||||
'student_id' => 999999,
|
||||
'class_section_id' => 999999,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Bad ids',
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->postJson('/api/assignments', $payload);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors([
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_class_assignment_data_returns_expected_structure(): void
|
||||
{
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
]);
|
||||
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $this->studentOne->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
'updated_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->getJson('/api/assignments/class-assignment-data');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('message', 'Class assignment data retrieved successfully.')
|
||||
->assertJsonPath('data.semester', 'Fall')
|
||||
->assertJsonPath('data.school_year', '2025-2026')
|
||||
->assertJsonCount(1, 'data.classSections')
|
||||
->assertJsonPath('data.classSections.0.class_section_name', 'Grade 1 - A')
|
||||
->assertJsonPath('data.classSections.0.main_teachers.0', 'Main Teacher')
|
||||
->assertJsonPath('data.classSections.0.students.0.firstname', 'Ali');
|
||||
}
|
||||
|
||||
public function test_class_assignment_data_deduplicates_teacher_names(): void
|
||||
{
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $this->teacherMain->id,
|
||||
'class_section_id' => $this->sectionA->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Morning group',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'sanctum')
|
||||
->getJson('/api/assignments/class-assignment-data');
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$mainTeachers = $response->json('data.classSections.0.main_teachers');
|
||||
|
||||
$this->assertSame(['Main Teacher'], $mainTeachers);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentApiTest extends TestCase
|
||||
@@ -39,7 +40,7 @@ class AssignmentApiTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
#[Test]
|
||||
public function it_returns_assignment_sections_json(): void
|
||||
{
|
||||
$section = ClassSection::factory()->create([
|
||||
@@ -108,7 +109,7 @@ class AssignmentApiTest extends TestCase
|
||||
$response->assertJsonPath('classSections.0.students.0.id', $student->id);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
#[Test]
|
||||
public function it_filters_assignments_by_school_year(): void
|
||||
{
|
||||
$section1 = ClassSection::factory()->create(['name' => 'Grade 4']);
|
||||
@@ -144,7 +145,7 @@ class AssignmentApiTest extends TestCase
|
||||
$response->assertJsonPath('selectedYear', '2025-2026');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
#[Test]
|
||||
public function it_creates_or_updates_student_assignment(): void
|
||||
{
|
||||
$student = Student::factory()->create(['is_active' => 1]);
|
||||
@@ -171,7 +172,7 @@ class AssignmentApiTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
#[Test]
|
||||
public function it_validates_required_fields_when_storing_assignment(): void
|
||||
{
|
||||
$response = $this->postJson('/api/assignments', []);
|
||||
@@ -183,7 +184,7 @@ class AssignmentApiTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
#[Test]
|
||||
public function store_is_idempotent_for_same_student_semester_and_year(): void
|
||||
{
|
||||
$student = Student::factory()->create(['is_active' => 1]);
|
||||
@@ -224,7 +225,7 @@ class AssignmentApiTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
#[Test]
|
||||
public function guest_cannot_access_assignment_api_when_auth_is_required(): void
|
||||
{
|
||||
auth()->guard('sanctum')->logout();
|
||||
@@ -233,4 +234,4 @@ class AssignmentApiTest extends TestCase
|
||||
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\AttendanceTracking;
|
||||
|
||||
use App\Services\AttendanceTrackingService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceTrackingControllerTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function mockService(): \Mockery\MockInterface
|
||||
{
|
||||
$service = Mockery::mock(AttendanceTrackingService::class);
|
||||
$this->app->instance(AttendanceTrackingService::class, $service);
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
public function test_pending_violations_returns_json_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('getPendingViolations')
|
||||
->once()
|
||||
->with('2025-2026', 'Fall')
|
||||
->andReturn([
|
||||
'students' => [
|
||||
['id' => 1, 'name' => 'John Doe'],
|
||||
],
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/attendance-tracking/pending-violations?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_notified_violations_returns_json_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('getNotifiedViolations')
|
||||
->once()
|
||||
->with('2025-2026', 'Fall')
|
||||
->andReturn([
|
||||
'students' => [],
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/attendance-tracking/notified-violations?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_student_case_returns_service_payload(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('getStudentCase')
|
||||
->once()
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'student' => ['id' => 10, 'name' => 'John Doe'],
|
||||
'attendance' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/attendance-tracking/student-case/10');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'student' => ['id' => 10, 'name' => 'John Doe'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_student_case_returns_service_status_code_when_not_found(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('getStudentCase')
|
||||
->once()
|
||||
->andReturn([
|
||||
'success' => false,
|
||||
'message' => 'Student not found',
|
||||
'status' => 404,
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/attendance-tracking/student-case/999');
|
||||
|
||||
$response->assertStatus(404)
|
||||
->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Student not found',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_record_validates_and_returns_service_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$payload = [
|
||||
'student_id' => 10,
|
||||
'date' => '2026-03-01',
|
||||
'parent_email' => 'parent@example.com',
|
||||
'parent_name' => 'Jane Doe',
|
||||
'subject_type' => 'Absent',
|
||||
];
|
||||
|
||||
$service->shouldReceive('record')
|
||||
->once()
|
||||
->with($payload)
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'message' => 'Notification queued.',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/attendance-tracking/record', $payload);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Notification queued.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_record_returns_validation_errors_for_bad_payload(): void
|
||||
{
|
||||
$response = $this->postJson('/api/attendance-tracking/record', [
|
||||
'student_id' => 0,
|
||||
'date' => 'bad-date',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['student_id', 'date']);
|
||||
}
|
||||
|
||||
public function test_send_auto_emails_returns_service_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('sendAutoEmails')
|
||||
->once()
|
||||
->with('default')
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'sent' => 2,
|
||||
'skipped' => 1,
|
||||
'errors' => 0,
|
||||
'details' => [],
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/attendance-tracking/send-auto-emails', [
|
||||
'variant' => 'default',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'sent' => 2,
|
||||
'skipped' => 1,
|
||||
'errors' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_compose_returns_service_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('compose')
|
||||
->once()
|
||||
->with(10, 'ABS_1', 'default', '2026-03-01')
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'student_id' => 10,
|
||||
'subject' => 'Test Subject',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/attendance-tracking/compose?student_id=10&code=ABS_1&variant=default&incident_date=2026-03-01');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'student_id' => 10,
|
||||
'subject' => 'Test Subject',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_send_manual_email_returns_service_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$payload = [
|
||||
'student_id' => 10,
|
||||
'to' => 'parent@example.com',
|
||||
'subject' => 'Hello',
|
||||
'body_html' => 'Body',
|
||||
'code' => 'ABS_1',
|
||||
'variant' => 'default',
|
||||
'incident_date' => '2026-03-01',
|
||||
];
|
||||
|
||||
$service->shouldReceive('sendEmailManual')
|
||||
->once()
|
||||
->with($payload)
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'message' => 'Email sent successfully.',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/attendance-tracking/send-manual-email', $payload);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Email sent successfully.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_send_manual_email_validates_payload(): void
|
||||
{
|
||||
$response = $this->postJson('/api/attendance-tracking/send-manual-email', [
|
||||
'student_id' => 0,
|
||||
'to' => 'not-an-email',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['student_id', 'to', 'subject', 'body_html', 'code']);
|
||||
}
|
||||
|
||||
public function test_parents_info_returns_service_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$service->shouldReceive('parentsInfo')
|
||||
->once()
|
||||
->with(10)
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'primary' => ['email' => 'parent@example.com'],
|
||||
'secondary' => ['email' => 'second@example.com'],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/attendance-tracking/parents-info?student_id=10');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'primary' => ['email' => 'parent@example.com'],
|
||||
'secondary' => ['email' => 'second@example.com'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_save_notification_note_returns_service_response(): void
|
||||
{
|
||||
$service = $this->mockService();
|
||||
|
||||
$payload = [
|
||||
'student_id' => 10,
|
||||
'code' => 'ABS_1',
|
||||
'note' => 'Called parent',
|
||||
'incident_date' => '2026-03-01',
|
||||
];
|
||||
|
||||
$service->shouldReceive('saveNotificationNote')
|
||||
->once()
|
||||
->with($payload)
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'message' => 'Note saved.',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/attendance-tracking/save-notification-note', $payload);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Note saved.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_save_notification_note_validates_payload(): void
|
||||
{
|
||||
$response = $this->postJson('/api/attendance-tracking/save-notification-note', [
|
||||
'student_id' => 0,
|
||||
'code' => '',
|
||||
'note' => '',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['student_id', 'code', 'note']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Badges\BadgeFormDataService;
|
||||
use App\Services\Badges\BadgePdfService;
|
||||
use App\Services\Badges\BadgePrintLogService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BadgeControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function authenticate(): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_form_data_returns_success_response(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
|
||||
$mock = Mockery::mock(BadgeFormDataService::class);
|
||||
$mock->shouldReceive('build')
|
||||
->once()
|
||||
->with('2025-2026', [10, 20], 'teacher')
|
||||
->andReturn([
|
||||
'users' => [],
|
||||
'schoolYears' => ['2025-2026'],
|
||||
'selectedYear' => '2025-2026',
|
||||
'selectedUserIds' => [10, 20],
|
||||
'rolesTabs' => ['teacher' => 'Teachers'],
|
||||
'active_role' => 'teacher',
|
||||
]);
|
||||
|
||||
$this->app->instance(BadgeFormDataService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/badges/form-data?school_year=2025-2026&user_ids=10,20&active_role=teacher');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'selectedYear' => '2025-2026',
|
||||
'selectedUserIds' => [10, 20],
|
||||
'active_role' => 'teacher',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_print_status_returns_success_response(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
|
||||
$mock = Mockery::mock(BadgePrintLogService::class);
|
||||
$mock->shouldReceive('getStatus')
|
||||
->once()
|
||||
->with([5, 7], '2025-2026')
|
||||
->andReturn([
|
||||
5 => [
|
||||
'count' => 2,
|
||||
'last_printed_at' => '2026-03-06 12:00:00',
|
||||
'last_printed_by' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->app->instance(BadgePrintLogService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/badges/print-status?user_ids=5,7&school_year=2025-2026');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'5' => [
|
||||
'count' => 2,
|
||||
'last_printed_at' => '2026-03-06 12:00:00',
|
||||
'last_printed_by' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_print_status_returns_500_when_service_throws(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
|
||||
$mock = Mockery::mock(BadgePrintLogService::class);
|
||||
$mock->shouldReceive('getStatus')
|
||||
->once()
|
||||
->andThrow(new \RuntimeException('db failed'));
|
||||
|
||||
$this->app->instance(BadgePrintLogService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/badges/print-status?user_ids=5,7');
|
||||
|
||||
$response->assertStatus(500)
|
||||
->assertJson([
|
||||
'ok' => false,
|
||||
'error' => 'Failed to query status',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_log_print_returns_success_response(): void
|
||||
{
|
||||
$authUser = $this->authenticate();
|
||||
|
||||
$mock = Mockery::mock(BadgePrintLogService::class);
|
||||
$mock->shouldReceive('log')
|
||||
->once()
|
||||
->with(
|
||||
[10, 20],
|
||||
$authUser->id,
|
||||
'2025-2026',
|
||||
['10' => 'Teacher'],
|
||||
['10' => '3-A']
|
||||
)
|
||||
->andReturn(2);
|
||||
|
||||
$this->app->instance(BadgePrintLogService::class, $mock);
|
||||
|
||||
$response = $this->postJson('/api/badges/log-print', [
|
||||
'user_ids' => [10, 20],
|
||||
'school_year' => '2025-2026',
|
||||
'roles' => ['10' => 'Teacher'],
|
||||
'classes' => ['10' => '3-A'],
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
'inserted' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_log_print_returns_500_when_service_throws(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
|
||||
$mock = Mockery::mock(BadgePrintLogService::class);
|
||||
$mock->shouldReceive('log')
|
||||
->once()
|
||||
->andThrow(new \RuntimeException('insert failed'));
|
||||
|
||||
$this->app->instance(BadgePrintLogService::class, $mock);
|
||||
|
||||
$response = $this->postJson('/api/badges/log-print', [
|
||||
'user_ids' => [10],
|
||||
]);
|
||||
|
||||
$response->assertStatus(500)
|
||||
->assertJson([
|
||||
'ok' => false,
|
||||
'error' => 'insert failed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_generate_pdf_returns_pdf_response(): void
|
||||
{
|
||||
$authUser = $this->authenticate();
|
||||
|
||||
$mock = Mockery::mock(BadgePdfService::class);
|
||||
$mock->shouldReceive('generate')
|
||||
->once()
|
||||
->with(
|
||||
[10, 20],
|
||||
'2025-2026',
|
||||
['10' => 'Teacher'],
|
||||
['10' => '3-A'],
|
||||
$authUser->id
|
||||
)
|
||||
->andReturn(response('fake-pdf-content', 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="Staff_Badges.pdf"',
|
||||
]));
|
||||
|
||||
$this->app->instance(BadgePdfService::class, $mock);
|
||||
|
||||
$response = $this->post('/api/badges/pdf', [
|
||||
'user_ids' => [10, 20],
|
||||
'school_year' => '2025-2026',
|
||||
'roles' => ['10' => 'Teacher'],
|
||||
'classes' => ['10' => '3-A'],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('Content-Type', 'application/pdf');
|
||||
$this->assertStringContainsString(
|
||||
'inline; filename="Staff_Badges.pdf"',
|
||||
$response->headers->get('Content-Disposition')
|
||||
);
|
||||
$this->assertSame('fake-pdf-content', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_generate_pdf_validates_required_user_ids(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
|
||||
$response = $this->postJson('/api/badges/pdf', [
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['user_ids']);
|
||||
}
|
||||
|
||||
public function test_log_print_validates_required_user_ids(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
|
||||
$response = $this->postJson('/api/badges/log-print', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['user_ids']);
|
||||
}
|
||||
|
||||
public function test_endpoints_require_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/badges/form-data')->assertStatus(401);
|
||||
$this->getJson('/api/badges/print-status')->assertStatus(401);
|
||||
$this->postJson('/api/badges/log-print', ['user_ids' => [1]])->assertStatus(401);
|
||||
$this->postJson('/api/badges/pdf', ['user_ids' => [1]])->assertStatus(401);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user