add controllers, servoices
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\AttendanceTracking;
|
||||
|
||||
use App\Services\AttendanceTrackingService;
|
||||
use App\Services\AttendanceTracking\AttendanceTrackingService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -323,4 +323,4 @@ class AttendanceTrackingControllerTest extends TestCase
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['student_id', 'code', 'note']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Auth;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegisterControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_captcha_endpoint_returns_value(): void
|
||||
{
|
||||
$response = $this->getJson('/api/v1/auth/register/captcha');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonStructure(['ok', 'captcha']);
|
||||
$this->assertNotEmpty($response->json('captcha'));
|
||||
}
|
||||
|
||||
public function test_register_creates_user(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
Role::query()->create([
|
||||
'id' => 1,
|
||||
'name' => 'parent',
|
||||
'slug' => 'parent',
|
||||
'description' => 'Parent',
|
||||
'priority' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'gender' => 'Male',
|
||||
'email' => 'parent@example.com',
|
||||
'confirm_email' => 'parent@example.com',
|
||||
'cellphone' => '5555555555',
|
||||
'address_street' => '123 Main',
|
||||
'apt' => 'A',
|
||||
'city' => 'City',
|
||||
'state' => 'CT',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'captcha' => 'ABCD',
|
||||
'is_parent' => 1,
|
||||
'no_second_parent_info' => 1,
|
||||
];
|
||||
|
||||
$response = $this->withSession(['captcha_answer' => 'ABCD'])
|
||||
->postJson('/api/v1/auth/register', $payload);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'parent@example.com',
|
||||
'status' => 'Inactive',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegisterPayloadDebugTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_debug_payload_shows_json_body(): void
|
||||
{
|
||||
$payload = [
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'gender' => 'Male',
|
||||
'email' => 'parent@example.com',
|
||||
'confirm_email' => 'parent@example.com',
|
||||
'cellphone' => '5555555555',
|
||||
'address_street' => '123 Main',
|
||||
'apt' => 'A',
|
||||
'city' => 'City',
|
||||
'state' => 'CT',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'captcha' => 'ABCD',
|
||||
'is_parent' => 1,
|
||||
'no_second_parent_info' => 1,
|
||||
];
|
||||
|
||||
$response = $this->withHeaders(['X-Debug-Request' => '1'])
|
||||
->postJson('/api/v1/auth/register', $payload);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('ok', true);
|
||||
$response->assertJsonFragment(['email' => 'parent@example.com']);
|
||||
$response->assertJsonPath('debug.json_all.email', 'parent@example.com');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\BroadcastEmail;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\EmailService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BroadcastEmailControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_options_returns_parents_and_senders(): void
|
||||
{
|
||||
$this->seedParentData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/broadcast-email/options');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('parents'));
|
||||
$this->assertNotEmpty($response->json('fromOptions'));
|
||||
}
|
||||
|
||||
public function test_send_test_only_dispatches_email(): void
|
||||
{
|
||||
$this->seedParentData();
|
||||
|
||||
$fake = new class extends EmailService {
|
||||
public array $sent = [];
|
||||
|
||||
public function send(
|
||||
array|string $to,
|
||||
string $subject,
|
||||
string $html,
|
||||
string $fromKey = 'general',
|
||||
array $cc = [],
|
||||
array $bcc = []
|
||||
): bool
|
||||
{
|
||||
$this->sent[] = compact('to', 'subject', 'html', 'fromKey', 'cc', 'bcc');
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$this->app->instance(EmailService::class, $fake);
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/broadcast-email/send', [
|
||||
'mode' => 'personalized',
|
||||
'subject' => 'Test Subject',
|
||||
'from_key' => 'general',
|
||||
'body_html' => '<p>Hello {{name}}</p>',
|
||||
'send_test_only' => true,
|
||||
'test_email' => 'parent@example.com',
|
||||
'wrap_layout' => false,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
$this->assertCount(1, $fake->sent);
|
||||
}
|
||||
|
||||
private function seedParentData(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
'id' => 1,
|
||||
'name' => 'parent',
|
||||
'slug' => 'parent',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 10,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\ClassPrep;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPrepControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_sticker_counts_returns_payload(): void
|
||||
{
|
||||
$this->seedStickerData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-prep/sticker-counts?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'totals' => [
|
||||
'primary' => 6,
|
||||
'secondary' => 1,
|
||||
'students' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_students_by_class_returns_roster(): void
|
||||
{
|
||||
$this->seedStickerData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-prep/classes/101/students?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertCount(1, $response->json('students'));
|
||||
$this->assertSame('1-A', $response->json('students.0.registration_grade'));
|
||||
}
|
||||
|
||||
private function seedStickerData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_name' => 'Class 1',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_name' => 'Class 2',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_name' => 'Class 3',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'class_section_name' => '5-B',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'class_section_name' => 'Youth-1',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
[
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-2',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Two',
|
||||
'age' => 10,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-3',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Three',
|
||||
'age' => 12,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\ClassPreparation;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_prep_payload(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-prep?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
'schoolYear' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
$this->assertNotEmpty($response->json('results'));
|
||||
}
|
||||
|
||||
public function test_mark_printed_creates_snapshots(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/class-prep/mark-printed', [
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'class_section_ids' => [101],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('class_preparation_log', [
|
||||
'class_section_id' => 101,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedPrepData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('inventory_categories')->insert([
|
||||
'type' => 'classroom',
|
||||
'name' => 'Small Table',
|
||||
]);
|
||||
|
||||
DB::table('inventory_items')->insert([
|
||||
'type' => 'classroom',
|
||||
'category_id' => 1,
|
||||
'name' => 'Small Table',
|
||||
'quantity' => 10,
|
||||
'good_qty' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Communication;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\EmailService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CommunicationControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_options_returns_students_and_templates(): void
|
||||
{
|
||||
$this->seedCommunicationData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/communications/options');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
$this->assertNotEmpty($response->json('templates'));
|
||||
}
|
||||
|
||||
public function test_preview_returns_rendered_subject_and_body(): void
|
||||
{
|
||||
$this->seedCommunicationData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/communications/preview', [
|
||||
'template_key' => 'welcome',
|
||||
'student_id' => 1,
|
||||
'family_id' => 1,
|
||||
'vars' => ['custom' => 'value'],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertStringContainsString('Parent One', (string) $response->json('subject'));
|
||||
$this->assertStringContainsString('Student One', (string) $response->json('html'));
|
||||
}
|
||||
|
||||
public function test_send_dispatches_email(): void
|
||||
{
|
||||
$this->seedCommunicationData();
|
||||
|
||||
$fake = new class extends EmailService {
|
||||
public array $sent = [];
|
||||
|
||||
public function send(
|
||||
array|string $to,
|
||||
string $subject,
|
||||
string $html,
|
||||
string $fromKey = 'general',
|
||||
array $cc = [],
|
||||
array $bcc = []
|
||||
): bool {
|
||||
$this->sent[] = compact('to', 'subject', 'html', 'fromKey', 'cc', 'bcc');
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$this->app->instance(EmailService::class, $fake);
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/communications/send', [
|
||||
'student_id' => 1,
|
||||
'family_id' => 1,
|
||||
'template_key' => 'welcome',
|
||||
'subject' => 'Hello',
|
||||
'body' => '<p>Hi</p>',
|
||||
'recipients' => ['parent1@example.com'],
|
||||
'cc' => ['parent2@example.com'],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertCount(1, $fake->sent);
|
||||
}
|
||||
|
||||
private function seedCommunicationData(): void
|
||||
{
|
||||
DB::table('students')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
'registration_grade' => '1-A',
|
||||
]);
|
||||
|
||||
DB::table('families')->insert([
|
||||
'id' => 1,
|
||||
'family_code' => 'F-1',
|
||||
'household_name' => 'Family One',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('family_students')->insert([
|
||||
'family_id' => 1,
|
||||
'student_id' => 1,
|
||||
'is_primary_home' => 1,
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 2,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent1@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('family_guardians')->insert([
|
||||
'family_id' => 1,
|
||||
'user_id' => 2,
|
||||
'relation' => 'parent',
|
||||
'is_primary' => 1,
|
||||
'receive_emails' => 1,
|
||||
'receive_sms' => 0,
|
||||
]);
|
||||
|
||||
DB::table('email_templates')->insert([
|
||||
'id' => 1,
|
||||
'code' => 'welcome',
|
||||
'variant' => 'default',
|
||||
'subject' => 'Hello {{parent_salutation}}',
|
||||
'body_html' => 'Hi {{student_fullname}}',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\CompetitionScores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CompetitionScoresControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_competitions_and_counts(): void
|
||||
{
|
||||
$this->seedCompetitionData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/competition-scores');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
'activeClassId' => 101,
|
||||
]);
|
||||
$this->assertNotEmpty($response->json('competitions'));
|
||||
$this->assertSame(1, $response->json('studentTotal'));
|
||||
}
|
||||
|
||||
public function test_edit_returns_students_and_scores(): void
|
||||
{
|
||||
$this->seedCompetitionData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/competition-scores/1');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
$this->assertSame(5, $response->json('scoreMap.1'));
|
||||
$this->assertSame(20, $response->json('questionCount'));
|
||||
}
|
||||
|
||||
public function test_save_updates_scores(): void
|
||||
{
|
||||
$this->seedCompetitionData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/competition-scores/1', [
|
||||
'class_section_id' => 101,
|
||||
'scores' => [
|
||||
1 => 10,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('competition_scores', [
|
||||
'competition_id' => 1,
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'score' => 10,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedCompetitionData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
'id' => 1,
|
||||
'class_name' => 'Class 1',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'class_section_id' => 101,
|
||||
'teacher_id' => 1,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('competitions')->insert([
|
||||
'id' => 1,
|
||||
'title' => 'Quiz',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'class_section_id' => 101,
|
||||
'is_locked' => 0,
|
||||
]);
|
||||
|
||||
DB::table('competition_class_winners')->insert([
|
||||
'competition_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'question_count' => 20,
|
||||
]);
|
||||
|
||||
DB::table('competition_scores')->insert([
|
||||
'competition_id' => 1,
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'score' => 5,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Discounts;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DiscountControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_options_returns_vouchers_and_parents(): void
|
||||
{
|
||||
$this->seedDiscountData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/discounts/options');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('vouchers'));
|
||||
$this->assertNotEmpty($response->json('parents'));
|
||||
}
|
||||
|
||||
public function test_apply_voucher_applies_discount(): void
|
||||
{
|
||||
$this->seedDiscountData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/discounts/apply', [
|
||||
'voucher_id' => 1,
|
||||
'parent_ids' => [10],
|
||||
'allow_additional' => true,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('discount_usages', [
|
||||
'voucher_id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_voucher_creates_row(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/discounts/vouchers', [
|
||||
'code' => 'NEW-10',
|
||||
'discount_type' => 'percent',
|
||||
'discount_value' => 10,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('discount_vouchers', ['code' => 'NEW-10']);
|
||||
}
|
||||
|
||||
public function test_show_voucher_returns_resource(): void
|
||||
{
|
||||
$this->seedDiscountData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/discounts/vouchers/1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
'voucher' => [
|
||||
'id' => 1,
|
||||
'code' => 'TEST-100',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_voucher_removes_row(): void
|
||||
{
|
||||
$this->seedDiscountData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->deleteJson('/api/v1/discounts/vouchers/1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseMissing('discount_vouchers', ['id' => 1]);
|
||||
}
|
||||
|
||||
private function seedDiscountData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
'id' => 1,
|
||||
'name' => 'parent',
|
||||
'slug' => 'parent',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 10,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('discount_vouchers')->insert([
|
||||
'id' => 1,
|
||||
'code' => 'TEST-100',
|
||||
'discount_type' => 'fixed',
|
||||
'discount_value' => 100,
|
||||
'max_uses' => 10,
|
||||
'times_used' => 0,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('invoices')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_number' => 'INV-1',
|
||||
'total_amount' => 100,
|
||||
'balance' => 100,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'Unpaid',
|
||||
'issue_date' => '2025-09-01',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('enrollments')->insert([
|
||||
'parent_id' => 10,
|
||||
'student_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'enrollment_status' => 'payment pending',
|
||||
'enrollment_date' => '2025-09-01',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Expenses;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExpenseControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_options_returns_staff_and_retailors(): void
|
||||
{
|
||||
$this->seedStaffUsers();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/expenses/options');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('staff'));
|
||||
$this->assertNotEmpty($response->json('retailors'));
|
||||
}
|
||||
|
||||
public function test_store_creates_expense(): void
|
||||
{
|
||||
$this->seedStaffUsers();
|
||||
|
||||
Storage::fake();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$file = UploadedFile::fake()->create('receipt.pdf', 10, 'application/pdf');
|
||||
|
||||
$response = $this->post('/api/v1/expenses', [
|
||||
'category' => 'Expense',
|
||||
'amount' => 25.5,
|
||||
'purchased_by' => 2,
|
||||
'retailor' => 'Amazon',
|
||||
'date_of_purchase' => '2025-09-01',
|
||||
'description' => 'Supplies',
|
||||
'receipt' => $file,
|
||||
], ['Accept' => 'application/json']);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'category' => 'Expense',
|
||||
'purchased_by' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_index_lists_expenses(): void
|
||||
{
|
||||
$this->seedExpenseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/expenses');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('expenses'));
|
||||
}
|
||||
|
||||
public function test_show_returns_expense(): void
|
||||
{
|
||||
$this->seedExpenseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/expenses/1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
'expense' => [
|
||||
'id' => 1,
|
||||
'category' => 'Expense',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_status_changes_status(): void
|
||||
{
|
||||
$this->seedExpenseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/expenses/1/status', [
|
||||
'status' => 'approved',
|
||||
'reason' => 'Ok',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'id' => 1,
|
||||
'status' => 'approved',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedStaffUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'administrator', 'slug' => 'administrator', 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 2,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Staff',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'staff@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 2, 'role_id' => 1],
|
||||
['user_id' => 3, 'role_id' => 2],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedExpenseData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 2,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Staff',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'staff@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('expenses')->insert([
|
||||
'id' => 1,
|
||||
'category' => 'Expense',
|
||||
'amount' => 10,
|
||||
'date_of_purchase' => '2025-09-01',
|
||||
'purchased_by' => 2,
|
||||
'added_by' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\ExtraCharges;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExtraChargesControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_returns_rows(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
$this->seedCharge();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/extra-charges');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('rows'));
|
||||
}
|
||||
|
||||
public function test_options_returns_parent_options(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/extra-charges/options?q=Parent');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('parents'));
|
||||
}
|
||||
|
||||
public function test_parent_options_returns_results(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/extra-charges/parents?q=Parent');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('results'));
|
||||
}
|
||||
|
||||
public function test_invoices_for_parent_returns_results(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/extra-charges/invoices?parent_id=10&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertNotEmpty($response->json('results'));
|
||||
}
|
||||
|
||||
public function test_store_creates_charge_and_updates_invoice(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/extra-charges', [
|
||||
'parent_id' => 10,
|
||||
'title' => 'Books',
|
||||
'amount' => 20,
|
||||
'charge_type' => 'add',
|
||||
'due_date' => '2025-09-01',
|
||||
'invoice_id' => 1,
|
||||
'description' => 'Extra books',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('additional_charges', [
|
||||
'parent_id' => 10,
|
||||
'title' => 'Books',
|
||||
'status' => 'applied',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'id' => 1,
|
||||
'total_amount' => 120,
|
||||
'balance' => 120,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_adjusts_charge(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
$this->seedCharge();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->putJson('/api/v1/extra-charges/1', [
|
||||
'title' => 'Books Updated',
|
||||
'amount' => 15,
|
||||
'charge_type' => 'add',
|
||||
'due_date' => '2025-09-01',
|
||||
'description' => 'Updated',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('additional_charges', [
|
||||
'id' => 1,
|
||||
'title' => 'Books Updated',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_void_changes_status(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
$this->seedCharge();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/extra-charges/1/void');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('additional_charges', [
|
||||
'id' => 1,
|
||||
'status' => 'void',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_reverse_changes_status(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
$this->seedCharge();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/extra-charges/1/reverse');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('additional_charges', [
|
||||
'id' => 1,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedBaseData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 10,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('invoices')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_number' => 'INV-1',
|
||||
'total_amount' => 100,
|
||||
'balance' => 100,
|
||||
'paid_amount' => 0,
|
||||
'issue_date' => '2025-09-01',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Unpaid',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedCharge(): void
|
||||
{
|
||||
DB::table('additional_charges')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'charge_type' => 'add',
|
||||
'title' => 'Books',
|
||||
'description' => 'Extra books',
|
||||
'amount' => 10,
|
||||
'due_date' => '2025-09-01',
|
||||
'status' => 'applied',
|
||||
'created_by' => 1,
|
||||
'created_at' => '2025-09-01 00:00:00',
|
||||
]);
|
||||
|
||||
DB::table('invoices')->where('id', 1)->update([
|
||||
'total_amount' => 110,
|
||||
'balance' => 110,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FinancialControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_report_returns_financial_report(): void
|
||||
{
|
||||
$this->seedFinancialData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/financial-report?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('report.invoices'));
|
||||
}
|
||||
|
||||
public function test_summary_returns_totals(): void
|
||||
{
|
||||
$this->seedFinancialData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/financial-summary?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertGreaterThan(0, (float) $response->json('summary.totalCharges'));
|
||||
}
|
||||
|
||||
public function test_unpaid_parents_lists_balances(): void
|
||||
{
|
||||
$this->seedFinancialData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/unpaid-parents?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('results'));
|
||||
}
|
||||
|
||||
private function seedFinancialData(): void
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 2,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('invoices')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'parent_id' => 2,
|
||||
'invoice_number' => 'INV-001',
|
||||
'total_amount' => 100,
|
||||
'balance' => 40,
|
||||
'paid_amount' => 60,
|
||||
'has_discount' => 1,
|
||||
'issue_date' => '2025-01-10',
|
||||
'due_date' => '2025-02-10',
|
||||
'status' => 'unpaid',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'parent_id' => 2,
|
||||
'invoice_number' => 'INV-002',
|
||||
'total_amount' => 50,
|
||||
'balance' => 50,
|
||||
'paid_amount' => 0,
|
||||
'has_discount' => 0,
|
||||
'issue_date' => '2025-02-01',
|
||||
'due_date' => '2025-03-01',
|
||||
'status' => 'unpaid',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('payments')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 2,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 60,
|
||||
'balance' => 40,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'Cash',
|
||||
'payment_date' => '2025-01-15',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Paid',
|
||||
]);
|
||||
|
||||
DB::table('discount_usages')->insert([
|
||||
'id' => 1,
|
||||
'voucher_id' => 1,
|
||||
'invoice_id' => 1,
|
||||
'parent_id' => 2,
|
||||
'discount_amount' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('refunds')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 2,
|
||||
'school_year' => '2025-2026',
|
||||
'invoice_id' => 1,
|
||||
'refund_amount' => 5,
|
||||
'status' => 'Paid',
|
||||
'refund_paid_amount' => 5,
|
||||
]);
|
||||
|
||||
DB::table('expenses')->insert([
|
||||
'id' => 1,
|
||||
'category' => 'Expense',
|
||||
'amount' => 20,
|
||||
'date_of_purchase' => '2025-01-05',
|
||||
'purchased_by' => 2,
|
||||
'added_by' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'status' => 'approved',
|
||||
]);
|
||||
|
||||
DB::table('reimbursements')->insert([
|
||||
'id' => 1,
|
||||
'expense_id' => 1,
|
||||
'amount' => 15,
|
||||
'reimbursed_to' => 2,
|
||||
'added_by' => 1,
|
||||
'status' => 'Paid',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvoiceControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_management_returns_parent_invoice_data(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/invoices/management?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('invoices'));
|
||||
}
|
||||
|
||||
public function test_generate_creates_invoice(): void
|
||||
{
|
||||
$this->seedBaseData(false);
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/invoices/generate', [
|
||||
'parent_id' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'parent_id' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_by_parent_returns_invoices(): void
|
||||
{
|
||||
$this->seedBaseData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/invoices/parent/10?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('invoices'));
|
||||
}
|
||||
|
||||
private function seedBaseData(bool $withInvoice = true): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 10,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
|
||||
DB::table('enrollments')->insert([
|
||||
'student_id' => 100,
|
||||
'parent_id' => 10,
|
||||
'class_section_id' => 1,
|
||||
'enrollment_status' => 'enrolled',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
if ($withInvoice) {
|
||||
DB::table('invoices')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_number' => 'INV-001',
|
||||
'total_amount' => 100,
|
||||
'balance' => 100,
|
||||
'paid_amount' => 0,
|
||||
'has_discount' => 0,
|
||||
'issue_date' => '2025-01-10',
|
||||
'status' => 'unpaid',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaymentControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_creates_payment_plan(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/payments', [
|
||||
'parent_id' => 10,
|
||||
'total_amount' => 250,
|
||||
'number_of_installments' => 2,
|
||||
'payment_date' => '2025-01-05',
|
||||
'payment_method' => 'cash',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('payments', [
|
||||
'parent_id' => 10,
|
||||
'total_amount' => 250,
|
||||
'number_of_installments' => 2,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_by_parent_returns_payments(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('payments')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 25,
|
||||
'balance' => 75,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'cash',
|
||||
'payment_date' => '2025-01-01',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Partially Paid',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/payments/parent/10?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('payments'));
|
||||
}
|
||||
|
||||
public function test_update_balance_updates_payment(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('payments')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 0,
|
||||
'balance' => 100,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'cash',
|
||||
'payment_date' => '2025-01-01',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Pending',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/payments/1/balance', [
|
||||
'paid_amount' => 25,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('payments', [
|
||||
'id' => 1,
|
||||
'paid_amount' => 25,
|
||||
'balance' => 75,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'admin', 'slug' => 'admin', 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 1, 'role_id' => 1],
|
||||
['user_id' => 10, 'role_id' => 2],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaymentEventChargesControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_creates_event_charges(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
$this->seedStudents();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/event-charges', [
|
||||
'parent_id' => 10,
|
||||
'event_name' => 'Field Trip',
|
||||
'description' => 'Bus fee',
|
||||
'amount' => 25,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'student_ids' => [100],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('event_charges', [
|
||||
'parent_id' => 10,
|
||||
'student_id' => 100,
|
||||
'event_name' => 'Field Trip',
|
||||
'amount' => 25,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'admin', 'slug' => 'admin', 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 1, 'role_id' => 1],
|
||||
['user_id' => 10, 'role_id' => 2],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedStudents(): void
|
||||
{
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaymentManualControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_search_returns_parent_data(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
$this->seedConfig();
|
||||
$this->seedStudent();
|
||||
$this->seedInvoice();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/manual-pay/search?search_term=parent@example.com');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertSame(10, $response->json('parent.id'));
|
||||
}
|
||||
|
||||
public function test_suggest_returns_items(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/manual-pay/suggest?q=parent');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('items'));
|
||||
}
|
||||
|
||||
private function seedUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'admin', 'slug' => 'admin', 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 1, 'role_id' => 1],
|
||||
['user_id' => 10, 'role_id' => 2],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedConfig(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['config_key' => 'installment_date', 'config_value' => '2025-06-01'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedStudent(): void
|
||||
{
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedInvoice(): void
|
||||
{
|
||||
DB::table('invoices')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_number' => 'INV-001',
|
||||
'total_amount' => 100,
|
||||
'balance' => 100,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'unpaid',
|
||||
'school_year' => '2025-2026',
|
||||
'issue_date' => '2025-01-01',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
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 PaymentNotificationControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_send_creates_log(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$this->seedUsers();
|
||||
$this->seedConfig();
|
||||
$this->seedInvoice();
|
||||
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/payment-notifications/send', [
|
||||
'parent_id' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('payment_notification_logs', [
|
||||
'parent_id' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_index_lists_logs(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
|
||||
DB::table('payment_notification_logs')->insert([
|
||||
'parent_id' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
'period_year' => 2025,
|
||||
'period_month' => 1,
|
||||
'type' => 'installment',
|
||||
'to_email' => 'parent@example.com',
|
||||
'status' => 'sent',
|
||||
'balance_snapshot' => 100,
|
||||
'sent_at' => '2025-01-01 10:00:00',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/payment-notifications');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('logs'));
|
||||
}
|
||||
|
||||
private function seedUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'admin', 'slug' => 'admin', 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 1, 'role_id' => 1],
|
||||
['user_id' => 10, 'role_id' => 2],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedConfig(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['config_key' => 'installment_date', 'config_value' => '2025-06-01'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedInvoice(): void
|
||||
{
|
||||
DB::table('invoices')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_number' => 'INV-001',
|
||||
'total_amount' => 200,
|
||||
'balance' => 200,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'unpaid',
|
||||
'school_year' => '2025-2026',
|
||||
'issue_date' => '2025-01-01',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaymentTransactionControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_creates_transaction(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
$this->seedPayment();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/payment-transactions', [
|
||||
'transaction_id' => 'TXN-1001',
|
||||
'payment_id' => 1,
|
||||
'transaction_date' => '2025-01-10',
|
||||
'amount' => 25,
|
||||
'payment_method' => 'cash',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('payment_transactions', [
|
||||
'transaction_id' => 'TXN-1001',
|
||||
'payment_id' => 1,
|
||||
'amount' => 25,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_by_payment_returns_transactions(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
$this->seedPayment();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('payment_transactions')->insert([
|
||||
'transaction_id' => 'TXN-1002',
|
||||
'payment_id' => 1,
|
||||
'transaction_date' => '2025-01-11',
|
||||
'amount' => 40,
|
||||
'payment_method' => 'cash',
|
||||
'payment_status' => 'Pending',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/payment-transactions/payment/1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('transactions'));
|
||||
}
|
||||
|
||||
public function test_update_status_updates_transaction(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
$this->seedPayment();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('payment_transactions')->insert([
|
||||
'transaction_id' => 'TXN-1003',
|
||||
'payment_id' => 1,
|
||||
'transaction_date' => '2025-01-11',
|
||||
'amount' => 40,
|
||||
'payment_method' => 'cash',
|
||||
'payment_status' => 'Pending',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/payment-transactions/TXN-1003/status', [
|
||||
'status' => 'Completed',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('payment_transactions', [
|
||||
'transaction_id' => 'TXN-1003',
|
||||
'payment_status' => 'Completed',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'admin', 'slug' => 'admin', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedPayment(): void
|
||||
{
|
||||
DB::table('payments')->insert([
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 0,
|
||||
'balance' => 100,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'cash',
|
||||
'payment_date' => '2025-01-01',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Pending',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaypalTransactionsControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_transactions(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('paypal_payments')->insert([
|
||||
'id' => 1,
|
||||
'transaction_id' => 'PP-1001',
|
||||
'order_id' => 'ORDER-1',
|
||||
'parent_school_id' => 10,
|
||||
'payer_email' => 'payer@example.com',
|
||||
'amount' => 50,
|
||||
'net_amount' => 48,
|
||||
'currency' => 'USD',
|
||||
'status' => 'COMPLETED',
|
||||
'event_type' => 'PAYMENT.SALE.COMPLETED',
|
||||
'created_at' => '2025-01-01 10:00:00',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/finance/paypal-transactions');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('transactions'));
|
||||
}
|
||||
|
||||
public function test_csv_download(): void
|
||||
{
|
||||
$this->seedUsers();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('paypal_payments')->insert([
|
||||
'id' => 1,
|
||||
'transaction_id' => 'PP-1002',
|
||||
'order_id' => 'ORDER-2',
|
||||
'parent_school_id' => 10,
|
||||
'payer_email' => 'payer@example.com',
|
||||
'amount' => 60,
|
||||
'net_amount' => 58,
|
||||
'currency' => 'USD',
|
||||
'status' => 'COMPLETED',
|
||||
'event_type' => 'PAYMENT.SALE.COMPLETED',
|
||||
'created_at' => '2025-01-01 10:00:00',
|
||||
]);
|
||||
|
||||
$response = $this->get('/api/v1/finance/paypal-transactions/csv');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('Content-Type', 'text/csv');
|
||||
}
|
||||
|
||||
private function seedUsers(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'admin', 'slug' => 'admin', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ReimbursementControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_create_batch_creates_open_batch(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('configuration')->insert([
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/reimbursements/batches', [
|
||||
'title' => 'Test Batch',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('reimbursement_batches', [
|
||||
'title' => 'Test Batch',
|
||||
'status' => 'open',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_mark_donation_updates_expense(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('expenses')->insert([
|
||||
'id' => 1,
|
||||
'category' => 'Expense',
|
||||
'amount' => 12.00,
|
||||
'date_of_purchase' => '2025-02-01',
|
||||
'added_by' => $user->id,
|
||||
'purchased_by' => $user->id,
|
||||
'status' => 'approved',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/reimbursements/mark-donation', [
|
||||
'expense_id' => 1,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'id' => 1,
|
||||
'category' => 'Donation',
|
||||
'status' => 'approved',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_reimbursement_creates_record_and_updates_expense(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 2,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Recipient',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'recipient@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('expenses')->insert([
|
||||
'id' => 2,
|
||||
'category' => 'Expense',
|
||||
'amount' => 50.00,
|
||||
'date_of_purchase' => '2025-02-05',
|
||||
'added_by' => $user->id,
|
||||
'purchased_by' => $user->id,
|
||||
'status' => 'approved',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/finance/reimbursements', [
|
||||
'expense_id' => 2,
|
||||
'amount' => 50.00,
|
||||
'reimbursed_to' => 2,
|
||||
'reimbursement_method' => 'Cash',
|
||||
'description' => 'Test reimbursement',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('reimbursements', [
|
||||
'expense_id' => 2,
|
||||
'amount' => 50.00,
|
||||
'reimbursed_to' => 2,
|
||||
'status' => 'Paid',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('expenses', [
|
||||
'id' => 2,
|
||||
'reimbursement_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_reimbursement_updates_fields(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('reimbursements')->insert([
|
||||
'id' => 10,
|
||||
'amount' => 25.00,
|
||||
'reimbursed_to' => $user->id,
|
||||
'added_by' => $user->id,
|
||||
'status' => 'Paid',
|
||||
'reimbursement_method' => 'Cash',
|
||||
]);
|
||||
|
||||
$response = $this->putJson('/api/v1/finance/reimbursements/10', [
|
||||
'amount' => 30.00,
|
||||
'reimbursed_to' => $user->id,
|
||||
'reimbursement_method' => 'Cash',
|
||||
'description' => 'Updated',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('reimbursements', [
|
||||
'id' => 10,
|
||||
'amount' => 30.00,
|
||||
'description' => 'Updated',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Grading;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GradingControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_overview_returns_grading_data(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->getJson('/api/v1/grading/overview?school_year=2025-2026&semester=Fall&class_id=1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('grades'));
|
||||
}
|
||||
|
||||
public function test_toggle_lock_locks_section(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/grading/locks/toggle', [
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('grading_locks', [
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'is_locked' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['id' => 3, 'config_key' => 'parent_scores_released_fall', 'config_value' => '0'],
|
||||
['id' => 4, 'config_key' => 'parent_scores_released_spring', 'config_value' => '0'],
|
||||
]);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'age' => 10,
|
||||
'gender' => 'M',
|
||||
'photo_consent' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
|
||||
DB::table('semester_scores')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 100,
|
||||
'school_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'semester_score' => 88,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Grading;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HomeworkTrackingControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_tracking_payload(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'teacher_id' => 1,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->getJson('/api/v1/grading/homework-tracking?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('teachers'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Incidents;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IncidentControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_current_returns_incidents_and_grades(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
$user = $this->seedUser();
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 10,
|
||||
'parent_id' => 99,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'Tester',
|
||||
'school_id' => '1',
|
||||
'age' => 10,
|
||||
'gender' => 'M',
|
||||
'photo_consent' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 10,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
|
||||
DB::table('current_incident')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 10,
|
||||
'student_name' => 'Kid Tester',
|
||||
'grade' => '1',
|
||||
'incident' => 'behavior',
|
||||
'incident_datetime' => '2025-01-01 10:00:00',
|
||||
'incident_state' => 'Open',
|
||||
'updated_by_open' => $user->id,
|
||||
'open_description' => 'Initial',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/incidents/current');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$response->assertJsonCount(1, 'incidents');
|
||||
$response->assertJsonCount(1, 'grades');
|
||||
$response->assertJsonPath('incidents.0.updated_by_open_name', 'Test User');
|
||||
}
|
||||
|
||||
public function test_store_and_close_moves_incident_to_history(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
$user = $this->seedUser();
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 11,
|
||||
'parent_id' => 99,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'Closer',
|
||||
'school_id' => '1',
|
||||
'age' => 11,
|
||||
'gender' => 'F',
|
||||
'photo_consent' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$storeResponse = $this->postJson('/api/v1/incidents', [
|
||||
'student_id' => 11,
|
||||
'grade' => '1',
|
||||
'incident' => 'behavior',
|
||||
'description' => 'Needs attention',
|
||||
]);
|
||||
|
||||
$storeResponse->assertOk();
|
||||
$storeResponse->assertJson(['ok' => true, 'created' => true]);
|
||||
$currentId = $storeResponse->json('incident_id');
|
||||
|
||||
$closeResponse = $this->postJson('/api/v1/incidents/' . $currentId . '/close', [
|
||||
'state_description' => 'Resolved',
|
||||
'action_taken' => 'Spoke with student',
|
||||
]);
|
||||
|
||||
$closeResponse->assertOk();
|
||||
$closeResponse->assertJson(['ok' => true]);
|
||||
|
||||
$historyId = $closeResponse->json('incident_id');
|
||||
|
||||
$this->assertDatabaseHas('incident', [
|
||||
'id' => $historyId,
|
||||
'student_id' => 11,
|
||||
'incident_state' => 'Closed',
|
||||
]);
|
||||
$this->assertDatabaseMissing('current_incident', ['id' => $currentId]);
|
||||
}
|
||||
|
||||
private function seedUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'test@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Reports;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FilesControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_receipt_meta_returns_file_payload(): void
|
||||
{
|
||||
$user = $this->seedUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$dir = storage_path('uploads/receipts');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'sample.pdf';
|
||||
file_put_contents($path, 'PDFDATA');
|
||||
|
||||
$response = $this->getJson('/api/v1/files/receipts/sample.pdf?meta=1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$response->assertJsonPath('file.name', 'sample.pdf');
|
||||
$response->assertJsonPath('file.size', 7);
|
||||
}
|
||||
|
||||
public function test_exam_draft_teacher_streams_with_download_name(): void
|
||||
{
|
||||
$user = $this->seedUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('exam_drafts')->insert([
|
||||
'id' => 1,
|
||||
'teacher_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'exam_type' => 'Midterm',
|
||||
'draft_title' => 'Draft',
|
||||
'teacher_file' => 'draft1.pdf',
|
||||
'version' => 2,
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$dir = storage_path('uploads/exams/drafts');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'draft1.pdf';
|
||||
file_put_contents($path, 'PDFDATA');
|
||||
|
||||
$response = $this->get('/api/v1/files/exams/teacher/draft1.pdf');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('Content-Disposition', 'inline; filename="1a_midterm_v2.pdf"');
|
||||
}
|
||||
|
||||
private function seedUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'test@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FinalControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_final_scores(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('final_exam')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'score' => 90,
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/final?class_section_id=1&semester=Spring&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HomeworkControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_homework_scores(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('homework')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'homework_index' => 1,
|
||||
'score' => 90,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/homework?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MidtermControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_midterm_scores(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('midterm_exam')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'score' => 85,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/midterm?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ParticipationControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_participation_scores(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('participation')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 100,
|
||||
'school_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'updated_by' => 1,
|
||||
'score' => 93,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/participation?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'age' => 10,
|
||||
'gender' => 'M',
|
||||
'photo_consent' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProjectControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_project_scores(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('project')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'project_index' => 1,
|
||||
'score' => 95,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/projects?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class QuizControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_quiz_scores(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('quiz')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'quiz_index' => 1,
|
||||
'score' => 88,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/quizzes?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScoreCommentControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_rejects_short_comment(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/scores/comments', [
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'comments' => [
|
||||
100 => [
|
||||
'ptap' => 'Too short',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_index_returns_comments(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('score_comments')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'score_type' => 'ptap',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'comment' => 'Kid is doing great in class and shows effort and focus in every session.',
|
||||
'commented_by' => 1,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/comments?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScoreControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_overview_returns_students(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'teacher_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/overview?class_section_id=1&semester=Fall&school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
public function test_lock_creates_grading_lock(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
$response = $this->postJson('/api/v1/scores/lock', [
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'missing_ok' => [],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertDatabaseHas('grading_locks', [
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'is_locked' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'age' => 10,
|
||||
'gender' => 'M',
|
||||
'photo_consent' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'is_event_only' => 0,
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Scores;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScorePredictorControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_predictions(): void
|
||||
{
|
||||
$this->seedBase();
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
|
||||
DB::table('semester_scores')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 100,
|
||||
'school_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'spring',
|
||||
'school_year' => '2025-2026',
|
||||
'semester_score' => 90,
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/scores/predictor?school_year=2025-2026&class_section_id=1');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$this->assertNotEmpty($response->json('students'));
|
||||
}
|
||||
|
||||
private function seedBase(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['id' => 3, 'config_key' => 'trophy_score', 'config_value' => '94'],
|
||||
['id' => 4, 'config_key' => 'pass_score', 'config_value' => '60'],
|
||||
]);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'teacher', 'slug' => 'teacher', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '1A',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 100,
|
||||
'parent_id' => 10,
|
||||
'firstname' => 'Kid',
|
||||
'lastname' => 'User',
|
||||
'school_id' => 1,
|
||||
'age' => 10,
|
||||
'gender' => 'M',
|
||||
'photo_consent' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 100,
|
||||
'class_section_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_event_only' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\System;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SemesterRangeControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_school_year_range_uses_config_dates(): void
|
||||
{
|
||||
$user = $this->seedUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'fall_semester_start', 'config_value' => '2025-09-05'],
|
||||
['id' => 2, 'config_key' => 'last_school_day', 'config_value' => '2026-06-15'],
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/system/semester-range/school-year?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('range.start_date', '2025-09-05');
|
||||
$response->assertJsonPath('range.end_date', '2026-06-15');
|
||||
}
|
||||
|
||||
public function test_resolve_returns_semester_for_date(): void
|
||||
{
|
||||
$user = $this->seedUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'fall_semester_start', 'config_value' => '2025-09-01'],
|
||||
['id' => 2, 'config_key' => 'spring_semester_start', 'config_value' => '2026-01-20'],
|
||||
['id' => 3, 'config_key' => 'last_school_day', 'config_value' => '2026-06-01'],
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/system/semester-range/resolve?date=2025-10-01');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('resolution.semester', 'Fall');
|
||||
}
|
||||
|
||||
private function seedUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'test@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Users;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_user_list_returns_users_with_roles(): void
|
||||
{
|
||||
$admin = $this->seedUser(1, 'admin@example.com');
|
||||
Sanctum::actingAs($admin);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
'id' => 10,
|
||||
'name' => 'Teacher',
|
||||
'slug' => 'teacher',
|
||||
'description' => 'Teacher role',
|
||||
'dashboard_route' => 'teacher/dashboard',
|
||||
'priority' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$this->seedUser(2, 'member@example.com');
|
||||
DB::table('user_roles')->insert([
|
||||
'user_id' => 2,
|
||||
'role_id' => 10,
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/users');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$response->assertJsonFragment(['email' => 'member@example.com']);
|
||||
$response->assertJsonFragment(['roles' => ['Teacher']]);
|
||||
}
|
||||
|
||||
public function test_create_user_creates_user_and_role(): void
|
||||
{
|
||||
$admin = $this->seedUser(1, 'admin@example.com');
|
||||
Sanctum::actingAs($admin);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
'id' => 20,
|
||||
'name' => 'Parent',
|
||||
'slug' => 'parent',
|
||||
'description' => 'Parent role',
|
||||
'dashboard_route' => 'parent/dashboard',
|
||||
'priority' => 2,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/users', [
|
||||
'firstname' => 'New',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5554443333',
|
||||
'email' => 'new.user@example.com',
|
||||
'address_street' => '123 Main St',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => true,
|
||||
'role_id' => 20,
|
||||
'password' => 'secret123',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJson(['ok' => true]);
|
||||
|
||||
$userId = (int) ($response->json('user_id') ?? 0);
|
||||
$this->assertGreaterThan(0, $userId);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $userId,
|
||||
'email' => 'new.user@example.com',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('user_roles', [
|
||||
'user_id' => $userId,
|
||||
'role_id' => 20,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_login_activity_returns_paginated_results(): void
|
||||
{
|
||||
$admin = $this->seedUser(1, 'admin@example.com');
|
||||
Sanctum::actingAs($admin);
|
||||
|
||||
DB::table('login_activity')->insert([
|
||||
'user_id' => 1,
|
||||
'email' => 'admin@example.com',
|
||||
'login_time' => '2025-01-02 09:00:00',
|
||||
'logout_time' => null,
|
||||
'ip_address' => '10.0.0.1',
|
||||
'user_agent' => 'TestAgent',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('login_activity')->insert([
|
||||
'user_id' => 1,
|
||||
'email' => 'admin@example.com',
|
||||
'login_time' => '2025-01-01 09:00:00',
|
||||
'logout_time' => null,
|
||||
'ip_address' => '10.0.0.2',
|
||||
'user_agent' => 'TestAgent',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/users/login-activity?per_page=1&page=2');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['ok' => true]);
|
||||
$response->assertJsonPath('pagination.total', 2);
|
||||
$response->assertJsonPath('pagination.currentPage', 2);
|
||||
$response->assertJsonPath('activities.0.ip_address', '10.0.0.2');
|
||||
}
|
||||
|
||||
private function seedUser(int $id, string $email): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => $id,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => $email,
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail($id);
|
||||
}
|
||||
}
|
||||
@@ -2,120 +2,54 @@
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Models\AdminNotificationSubject;
|
||||
use App\Services\Administrator\AdminNotificationSubjectService;
|
||||
use App\Services\Administrator\AdminNotificationUserService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminNotificationSubjectServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_alerts_data_includes_admins(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_alerts_data_returns_expected_payload_when_table_exists(): void
|
||||
{
|
||||
DB::shouldReceive('getSchemaBuilder->hasTable')
|
||||
->once()
|
||||
->with('admin_notification_subjects')
|
||||
->andReturn(true);
|
||||
|
||||
$userService = Mockery::mock(AdminNotificationUserService::class);
|
||||
$userService->shouldReceive('fetchAdminNotificationUsers')
|
||||
->once()
|
||||
->andReturn([
|
||||
['id' => 1, 'firstname' => 'Admin', 'lastname' => 'One', 'email' => 'a1@test.com'],
|
||||
['id' => 2, 'firstname' => 'Admin', 'lastname' => 'Two', 'email' => 'a2@test.com'],
|
||||
]);
|
||||
|
||||
$row1 = (object) ['id' => 10, 'admin_id' => 1, 'subject' => 'academics'];
|
||||
$row2 = (object) ['id' => 11, 'admin_id' => 1, 'subject' => 'finance'];
|
||||
$row3 = (object) ['id' => 12, 'admin_id' => 2, 'subject' => 'general'];
|
||||
|
||||
AdminNotificationSubject::shouldReceive('query->select->whereIn->get')
|
||||
->once()
|
||||
->andReturn(collect([$row1, $row2, $row3]));
|
||||
|
||||
$service = new AdminNotificationSubjectService($userService);
|
||||
|
||||
$result = $service->alertsData();
|
||||
|
||||
$this->assertTrue($result['tableReady']);
|
||||
$this->assertCount(2, $result['admins']);
|
||||
$this->assertArrayHasKey('academics', $result['subjects']);
|
||||
$this->assertTrue($result['assignedSubjects'][1]['academics']);
|
||||
$this->assertTrue($result['assignedSubjects'][1]['finance']);
|
||||
$this->assertTrue($result['assignedSubjects'][2]['general']);
|
||||
}
|
||||
|
||||
public function test_save_returns_error_when_table_missing(): void
|
||||
{
|
||||
DB::shouldReceive('getSchemaBuilder->hasTable')
|
||||
->once()
|
||||
->with('admin_notification_subjects')
|
||||
->andReturn(false);
|
||||
|
||||
$userService = Mockery::mock(AdminNotificationUserService::class);
|
||||
$service = new AdminNotificationSubjectService($userService);
|
||||
|
||||
$result = $service->save([
|
||||
1 => ['academics'],
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'Admin', 'slug' => 'admin', 'dashboard_route' => 'admin', 'priority' => 1, 'is_active' => 1],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
|
||||
public function test_save_inserts_and_deletes_subjects(): void
|
||||
{
|
||||
DB::shouldReceive('getSchemaBuilder->hasTable')
|
||||
->once()
|
||||
->with('admin_notification_subjects')
|
||||
->andReturn(true);
|
||||
|
||||
$userService = Mockery::mock(AdminNotificationUserService::class);
|
||||
$userService->shouldReceive('fetchAdminNotificationUsers')
|
||||
->once()
|
||||
->andReturn([
|
||||
['id' => 1, 'firstname' => 'Admin', 'lastname' => 'One', 'email' => 'a1@test.com'],
|
||||
['id' => 2, 'firstname' => 'Admin', 'lastname' => 'Two', 'email' => 'a2@test.com'],
|
||||
]);
|
||||
|
||||
$existing1 = (object) ['id' => 101, 'admin_id' => 1, 'subject' => 'academics'];
|
||||
$existing2 = (object) ['id' => 102, 'admin_id' => 1, 'subject' => 'finance'];
|
||||
$existing3 = (object) ['id' => 103, 'admin_id' => 2, 'subject' => 'general'];
|
||||
|
||||
AdminNotificationSubject::shouldReceive('query->select->whereIn->get')
|
||||
->once()
|
||||
->andReturn(collect([$existing1, $existing2, $existing3]));
|
||||
|
||||
AdminNotificationSubject::shouldReceive('query->where->whereIn->delete')
|
||||
->once()
|
||||
->andReturn(1);
|
||||
|
||||
AdminNotificationSubject::shouldReceive('insert')
|
||||
->once()
|
||||
->with(Mockery::on(function ($batch) {
|
||||
return count($batch) === 2
|
||||
&& $batch[0]['admin_id'] === 1
|
||||
&& $batch[0]['subject'] === 'general'
|
||||
&& $batch[1]['admin_id'] === 2
|
||||
&& $batch[1]['subject'] === 'attendance';
|
||||
}))
|
||||
->andReturn(true);
|
||||
|
||||
$service = new AdminNotificationSubjectService($userService);
|
||||
|
||||
$result = $service->save([
|
||||
1 => ['academics', 'general'],
|
||||
2 => ['general', 'attendance'],
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(200, $result['status']);
|
||||
DB::table('user_roles')->insert([
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
$userService = new \App\Services\Administrator\AdminNotificationUserService();
|
||||
$service = new AdminNotificationSubjectService($userService);
|
||||
|
||||
$data = $service->alertsData();
|
||||
|
||||
$this->assertTrue($data['tableReady']);
|
||||
$this->assertCount(1, $data['admins']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdminNotificationUserService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminNotificationUserServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_fetch_admin_notification_users_excludes_parents(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'Admin', 'slug' => 'admin', 'dashboard_route' => 'admin', 'priority' => 1, 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'Parent', 'slug' => 'parent', 'dashboard_route' => 'parent', 'priority' => 2, 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['id' => 1, 'user_id' => 1, 'role_id' => 1],
|
||||
['id' => 2, 'user_id' => 2, 'role_id' => 2],
|
||||
]);
|
||||
|
||||
$service = new AdminNotificationUserService();
|
||||
$admins = $service->fetchAdminNotificationUsers();
|
||||
|
||||
$this->assertCount(1, $admins);
|
||||
$this->assertSame('admin@example.com', $admins[0]['email']);
|
||||
}
|
||||
}
|
||||
@@ -2,111 +2,57 @@
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Models\AdminNotificationSubject;
|
||||
use App\Services\Administrator\AdminNotificationUserService;
|
||||
use App\Services\Administrator\AdminPrintRecipientService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPrintRecipientServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_data_returns_admins_and_assigned(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_data_returns_assigned_admins(): void
|
||||
{
|
||||
DB::shouldReceive('getSchemaBuilder->hasTable')
|
||||
->once()
|
||||
->with('admin_notification_subjects')
|
||||
->andReturn(true);
|
||||
|
||||
$userService = Mockery::mock(AdminNotificationUserService::class);
|
||||
$userService->shouldReceive('fetchAdminNotificationUsers')
|
||||
->once()
|
||||
->andReturn([
|
||||
['id' => 1, 'firstname' => 'A', 'lastname' => 'One', 'email' => 'a1@test.com'],
|
||||
['id' => 2, 'firstname' => 'A', 'lastname' => 'Two', 'email' => 'a2@test.com'],
|
||||
]);
|
||||
|
||||
$row1 = (object) ['admin_id' => 1];
|
||||
$row2 = (object) ['admin_id' => 2];
|
||||
|
||||
AdminNotificationSubject::shouldReceive('query->select->where->whereIn->get')
|
||||
->once()
|
||||
->andReturn(collect([$row1, $row2]));
|
||||
|
||||
$service = new AdminPrintRecipientService($userService);
|
||||
|
||||
$result = $service->data();
|
||||
|
||||
$this->assertTrue($result['tableReady']);
|
||||
$this->assertTrue($result['assigned'][1]);
|
||||
$this->assertTrue($result['assigned'][2]);
|
||||
}
|
||||
|
||||
public function test_save_returns_error_when_table_missing(): void
|
||||
{
|
||||
DB::shouldReceive('getSchemaBuilder->hasTable')
|
||||
->once()
|
||||
->with('admin_notification_subjects')
|
||||
->andReturn(false);
|
||||
|
||||
$userService = Mockery::mock(AdminNotificationUserService::class);
|
||||
$service = new AdminPrintRecipientService($userService);
|
||||
|
||||
$result = $service->save([
|
||||
1 => 1,
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'Admin', 'slug' => 'admin', 'dashboard_route' => 'admin', 'priority' => 1, 'is_active' => 1],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
|
||||
public function test_save_updates_print_recipients(): void
|
||||
{
|
||||
DB::shouldReceive('getSchemaBuilder->hasTable')
|
||||
->once()
|
||||
->with('admin_notification_subjects')
|
||||
->andReturn(true);
|
||||
|
||||
$userService = Mockery::mock(AdminNotificationUserService::class);
|
||||
$userService->shouldReceive('fetchAdminNotificationUsers')
|
||||
->once()
|
||||
->andReturn([
|
||||
['id' => 1],
|
||||
['id' => 2],
|
||||
['id' => 3],
|
||||
]);
|
||||
|
||||
AdminNotificationSubject::shouldReceive('query->where->whereIn->pluck')
|
||||
->once()
|
||||
->andReturn(collect([1, 2]));
|
||||
|
||||
AdminNotificationSubject::shouldReceive('query->where->whereIn->delete')
|
||||
->once()
|
||||
->andReturn(1);
|
||||
|
||||
AdminNotificationSubject::shouldReceive('insert')
|
||||
->once()
|
||||
->with(Mockery::on(function ($batch) {
|
||||
return count($batch) === 1
|
||||
&& $batch[0]['admin_id'] === 3
|
||||
&& $batch[0]['subject'] === 'print_requests';
|
||||
}))
|
||||
->andReturn(true);
|
||||
|
||||
$service = new AdminPrintRecipientService($userService);
|
||||
|
||||
$result = $service->save([
|
||||
1 => 1,
|
||||
3 => 1,
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(200, $result['status']);
|
||||
DB::table('user_roles')->insert([
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
|
||||
DB::table('admin_notification_subjects')->insert([
|
||||
'id' => 1,
|
||||
'admin_id' => 1,
|
||||
'subject' => 'print_requests',
|
||||
]);
|
||||
|
||||
$service = new AdminPrintRecipientService(new \App\Services\Administrator\AdminNotificationUserService());
|
||||
$data = $service->data();
|
||||
|
||||
$this->assertTrue($data['assigned'][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorAbsenceService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorAbsenceServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_submit_requires_reason(): void
|
||||
{
|
||||
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('allowedAbsenceDates')->andReturn(['2025-01-05']);
|
||||
|
||||
$service = new AdministratorAbsenceService(
|
||||
$shared,
|
||||
new \App\Models\User(),
|
||||
new \App\Models\StaffAttendance(),
|
||||
new \App\Services\SemesterRangeService(new \App\Services\Semesters\SemesterConfigService()),
|
||||
Mockery::mock(\App\Libraries\StaffTimeOffLinkService::class)
|
||||
);
|
||||
|
||||
$request = Request::create('/absences', 'POST', ['dates' => ['2025-01-05']]);
|
||||
$result = $service->submit($request, 1);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorDashboardService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorDashboardServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_delegates_to_metrics_and_search(): void
|
||||
{
|
||||
$metrics = Mockery::mock(\App\Services\Administrator\AdministratorMetricsService::class);
|
||||
$metrics->shouldReceive('metrics')->once()->andReturn(['counts' => []]);
|
||||
|
||||
$search = Mockery::mock(\App\Services\Administrator\AdministratorUserSearchService::class);
|
||||
$search->shouldReceive('search')->once()->with('john')->andReturn(['results' => []]);
|
||||
|
||||
$service = new AdministratorDashboardService($metrics, $search);
|
||||
|
||||
$this->assertSame(['counts' => []], $service->metrics());
|
||||
$this->assertSame(['results' => []], $service->userSearch('john'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorEnrollmentEventService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorEnrollmentEventServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_dispatch_grouped_events_with_empty_groups(): void
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
$service = new AdministratorEnrollmentEventService();
|
||||
$service->dispatchGroupedEvents([], [], [], '2025-2026');
|
||||
|
||||
Event::assertNotDispatched('studentEnrolled');
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,8 @@
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Services\Administrator\AdministratorEnrollmentQueryService;
|
||||
use App\Services\Administrator\AdministratorSharedService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -18,253 +11,24 @@ class AdministratorEnrollmentQueryServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
public function test_enrollment_withdrawal_data_returns_structure(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function makeSharedService(string $schoolYear = '2025-2026', string $semester = 'Fall'): AdministratorSharedService
|
||||
{
|
||||
$configuration = Mockery::mock(Configuration::class);
|
||||
$configuration->shouldReceive('getConfig')
|
||||
->with('school_year')
|
||||
->andReturn($schoolYear);
|
||||
$configuration->shouldReceive('getConfig')
|
||||
->with('semester')
|
||||
->andReturn($semester);
|
||||
|
||||
return new AdministratorSharedService($configuration);
|
||||
}
|
||||
|
||||
public function test_enrollment_withdrawal_data_returns_students_classes_and_school_years(): void
|
||||
{
|
||||
DB::table('enrollments')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'parent_id' => 100,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'enrollment_status' => 'enrolled',
|
||||
'admission_status' => 'accepted',
|
||||
'is_withdrawn' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'parent_id' => 101,
|
||||
'school_year' => '2024-2025',
|
||||
'semester' => 'Fall',
|
||||
'enrollment_status' => 'withdrawn',
|
||||
'admission_status' => 'accepted',
|
||||
'is_withdrawn' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$studentModel->shouldReceive('getStudentsWithClassAndEnrollment')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'id' => 1,
|
||||
'firstname' => 'Ali',
|
||||
'lastname' => 'Ben',
|
||||
'parent_id' => 100,
|
||||
'parent_firstname' => 'Parent',
|
||||
'parent_lastname' => 'One',
|
||||
'is_new' => 1,
|
||||
'registration_date' => '2025-09-10 08:00:00',
|
||||
'admission_status' => 'accepted',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Ali',
|
||||
'parent_id' => 101,
|
||||
'parent_firstname' => 'Parent',
|
||||
'parent_lastname' => 'Two',
|
||||
'is_new' => 0,
|
||||
'registration_date' => '2024-09-10 08:00:00',
|
||||
'admission_status' => 'accepted',
|
||||
],
|
||||
]);
|
||||
|
||||
$studentClassModel = Mockery::mock(StudentClass::class);
|
||||
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
|
||||
->with(1, '2025-2026')
|
||||
->andReturn('Grade 5A');
|
||||
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
|
||||
->with(2, '2025-2026')
|
||||
->andReturn('');
|
||||
|
||||
$enrollmentModel = Mockery::mock(Enrollment::class);
|
||||
$enrollmentModel->shouldReceive('getEnrollmentStatus')
|
||||
->with(1, '2025-2026')
|
||||
->andReturn('enrolled');
|
||||
$enrollmentModel->shouldReceive('getEnrollmentStatus')
|
||||
->with(2, '2025-2026')
|
||||
->andReturn('');
|
||||
|
||||
ClassSection::unguard();
|
||||
ClassSection::create([
|
||||
'class_section_id' => 10,
|
||||
'class_section_name' => 'Grade 5A',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
$shared->shouldReceive('getSchoolYearStartYear')->andReturn(2025);
|
||||
|
||||
$service = new AdministratorEnrollmentQueryService(
|
||||
$this->makeSharedService(),
|
||||
$studentModel,
|
||||
$studentClassModel,
|
||||
$enrollmentModel,
|
||||
new ClassSection()
|
||||
$shared,
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\StudentClass(),
|
||||
new \App\Models\Enrollment(),
|
||||
new \App\Models\ClassSection()
|
||||
);
|
||||
|
||||
$result = $service->enrollmentWithdrawalData('2025-2026');
|
||||
$data = $service->enrollmentWithdrawalData('2025-2026');
|
||||
|
||||
$this->assertSame('2025-2026', $result['school_year']);
|
||||
$this->assertSame('Fall', $result['semester']);
|
||||
$this->assertNotEmpty($result['schoolYears']);
|
||||
$this->assertCount(2, $result['students']);
|
||||
$this->assertCount(1, $result['classes']);
|
||||
|
||||
$student1 = collect($result['students'])->firstWhere('student_id', 1);
|
||||
$student2 = collect($result['students'])->firstWhere('student_id', 2);
|
||||
|
||||
$this->assertSame('Yes', $student1['new_student']);
|
||||
$this->assertSame('Grade 5A', $student1['class_section']);
|
||||
$this->assertSame('enrolled', $student1['enrollment_status']);
|
||||
$this->assertSame('2025-09-10', $student1['registration_date_order']);
|
||||
|
||||
$this->assertSame('No', $student2['new_student']);
|
||||
$this->assertSame('Class not Assigned', $student2['class_section']);
|
||||
$this->assertArrayHasKey('students', $data);
|
||||
$this->assertArrayHasKey('classes', $data);
|
||||
}
|
||||
|
||||
public function test_enrollment_withdrawal_data_marks_removed_previous_year(): void
|
||||
{
|
||||
DB::table('enrollments')->insert([
|
||||
[
|
||||
'student_id' => 5,
|
||||
'parent_id' => 200,
|
||||
'school_year' => '2024-2025',
|
||||
'semester' => 'Fall',
|
||||
'enrollment_status' => 'withdrawn',
|
||||
'admission_status' => 'accepted',
|
||||
'is_withdrawn' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$studentModel->shouldReceive('getStudentsWithClassAndEnrollment')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'id' => 5,
|
||||
'firstname' => 'Mina',
|
||||
'lastname' => 'K',
|
||||
'parent_id' => 200,
|
||||
'parent_firstname' => 'Parent',
|
||||
'parent_lastname' => 'Three',
|
||||
'is_new' => 0,
|
||||
'registration_date' => '2024-08-01 00:00:00',
|
||||
'admission_status' => 'accepted',
|
||||
],
|
||||
]);
|
||||
|
||||
$studentClassModel = Mockery::mock(StudentClass::class);
|
||||
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
|
||||
->with(5, '2025-2026')
|
||||
->andReturn('Grade 6A');
|
||||
|
||||
$enrollmentModel = Mockery::mock(Enrollment::class);
|
||||
$enrollmentModel->shouldReceive('getEnrollmentStatus')
|
||||
->with(5, '2025-2026')
|
||||
->andReturn('payment pending');
|
||||
|
||||
$service = new AdministratorEnrollmentQueryService(
|
||||
$this->makeSharedService('2025-2026', 'Fall'),
|
||||
$studentModel,
|
||||
$studentClassModel,
|
||||
$enrollmentModel,
|
||||
new ClassSection()
|
||||
);
|
||||
|
||||
$result = $service->enrollmentWithdrawalData('2025-2026');
|
||||
|
||||
$student = $result['students'][0];
|
||||
|
||||
$this->assertSame('Yes', $student['removed_previous_year']);
|
||||
$this->assertSame('payment pending', $student['enrollment_status']);
|
||||
}
|
||||
|
||||
public function test_new_students_data_returns_transformed_rows(): void
|
||||
{
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$studentModel->shouldReceive('getStudentsWithParentsAndEmergency')
|
||||
->once()
|
||||
->with('2025-2026')
|
||||
->andReturn([
|
||||
[
|
||||
'id' => 9,
|
||||
'firstname' => 'Adam',
|
||||
'lastname' => 'Y',
|
||||
'is_new' => 1,
|
||||
'registration_date' => '2025-10-01 10:00:00',
|
||||
],
|
||||
[
|
||||
'id' => 10,
|
||||
'firstname' => 'Eve',
|
||||
'lastname' => 'Z',
|
||||
'is_new' => 0,
|
||||
'registration_date' => null,
|
||||
],
|
||||
]);
|
||||
|
||||
$studentClassModel = Mockery::mock(StudentClass::class);
|
||||
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
|
||||
->with(9, '2025-2026')
|
||||
->andReturn('Grade 1A');
|
||||
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
|
||||
->with(10, '2025-2026')
|
||||
->andReturn('');
|
||||
|
||||
$enrollmentModel = Mockery::mock(Enrollment::class);
|
||||
$enrollmentModel->shouldReceive('getEnrollmentStatus')
|
||||
->with(9, '2025-2026')
|
||||
->andReturn('enrolled');
|
||||
$enrollmentModel->shouldReceive('getEnrollmentStatus')
|
||||
->with(10, '2025-2026')
|
||||
->andReturn('waitlist');
|
||||
|
||||
$service = new AdministratorEnrollmentQueryService(
|
||||
$this->makeSharedService(),
|
||||
$studentModel,
|
||||
$studentClassModel,
|
||||
$enrollmentModel,
|
||||
new ClassSection()
|
||||
);
|
||||
|
||||
$result = $service->newStudentsData();
|
||||
|
||||
$this->assertSame(2, $result['total_new']);
|
||||
|
||||
$first = $result['new_students'][0];
|
||||
$second = $result['new_students'][1];
|
||||
|
||||
$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,34 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorEnrollmentRefundService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorEnrollmentRefundServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_process_refunds_reports_missing_invoice(): void
|
||||
{
|
||||
DB::table('enrollments')->insert([
|
||||
'id' => 1,
|
||||
'student_id' => 1,
|
||||
'parent_id' => 10,
|
||||
'enrollment_date' => '2025-01-01',
|
||||
'enrollment_status' => 'withdrawn',
|
||||
'is_withdrawn' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$feeCalc = Mockery::mock(\App\Services\FeeCalculationService::class);
|
||||
$service = new AdministratorEnrollmentRefundService($feeCalc);
|
||||
|
||||
$result = $service->processRefunds([10], '2025-2026', 1);
|
||||
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorEnrollmentService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorEnrollmentServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_delegates_to_query_and_status_services(): void
|
||||
{
|
||||
$query = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentQueryService::class);
|
||||
$query->shouldReceive('enrollmentWithdrawalData')->once()->andReturn(['rows' => []]);
|
||||
$query->shouldReceive('newStudentsData')->once()->andReturn(['students' => []]);
|
||||
|
||||
$status = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentStatusService::class);
|
||||
$status->shouldReceive('updateStatuses')->once()->andReturn(['success' => true]);
|
||||
|
||||
$service = new AdministratorEnrollmentService($query, $status);
|
||||
|
||||
$this->assertSame(['rows' => []], $service->enrollmentWithdrawalData());
|
||||
$this->assertSame(['students' => []], $service->showNewStudentsData());
|
||||
$this->assertSame(['success' => true], $service->updateStatuses([], 1));
|
||||
}
|
||||
}
|
||||
@@ -2,236 +2,35 @@
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use App\Services\Administrator\AdministratorEnrollmentEventService;
|
||||
use App\Services\Administrator\AdministratorEnrollmentRefundService;
|
||||
use App\Services\Administrator\AdministratorEnrollmentStatusService;
|
||||
use App\Services\Administrator\AdministratorSharedService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorEnrollmentStatusServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_update_statuses_returns_validation_error_when_empty(): void
|
||||
public function test_update_statuses_requires_payload(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$userModel = Mockery::mock(User::class);
|
||||
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
|
||||
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
|
||||
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
|
||||
$refundService = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentRefundService::class);
|
||||
$eventService = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentEventService::class);
|
||||
|
||||
$service = new AdministratorEnrollmentStatusService(
|
||||
$shared,
|
||||
$studentModel,
|
||||
$userModel,
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\User(),
|
||||
$refundService,
|
||||
$eventService
|
||||
);
|
||||
|
||||
$result = $service->updateStatuses([], 99);
|
||||
$result = $service->updateStatuses([], 1);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
|
||||
public function test_update_statuses_creates_new_enrollment_and_dispatches_events(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$studentModel->shouldReceive('find')
|
||||
->once()
|
||||
->with(10)
|
||||
->andReturn([
|
||||
'id' => 10,
|
||||
'firstname' => 'Ali',
|
||||
'lastname' => 'Ben',
|
||||
'parent_id' => 20,
|
||||
]);
|
||||
|
||||
$userModel = Mockery::mock(User::class);
|
||||
$userModel->shouldReceive('find')
|
||||
->once()
|
||||
->with(20)
|
||||
->andReturn([
|
||||
'id' => 20,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'One',
|
||||
'email' => 'parent@test.com',
|
||||
]);
|
||||
|
||||
DB::shouldReceive('beginTransaction')->once();
|
||||
DB::shouldReceive('rollBack')->never();
|
||||
DB::shouldReceive('commit')->once();
|
||||
|
||||
DB::shouldReceive('table->where->where->first')
|
||||
->once()
|
||||
->andReturn(null);
|
||||
|
||||
DB::shouldReceive('table->insert')
|
||||
->once()
|
||||
->andReturn(true);
|
||||
|
||||
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
|
||||
$refundService->shouldReceive('processRefunds')
|
||||
->once()
|
||||
->with([], '2025-2026', 77)
|
||||
->andReturn([
|
||||
'errors' => [],
|
||||
'refundAmountByParent' => [],
|
||||
]);
|
||||
|
||||
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
|
||||
$eventService->shouldReceive('dispatchGroupedEvents')
|
||||
->once()
|
||||
->with(
|
||||
Mockery::on(function ($groups) {
|
||||
return isset($groups[20]['enrolled'][0]['student_id'])
|
||||
&& $groups[20]['enrolled'][0]['student_id'] === 10;
|
||||
}),
|
||||
Mockery::on(function ($parents) {
|
||||
return isset($parents[20]['email']) && $parents[20]['email'] === 'parent@test.com';
|
||||
}),
|
||||
[],
|
||||
'2025-2026'
|
||||
);
|
||||
|
||||
$service = new AdministratorEnrollmentStatusService(
|
||||
$shared,
|
||||
$studentModel,
|
||||
$userModel,
|
||||
$refundService,
|
||||
$eventService
|
||||
);
|
||||
|
||||
$result = $service->updateStatuses([
|
||||
10 => 'enrolled',
|
||||
], 77);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(200, $result['status']);
|
||||
}
|
||||
|
||||
public function test_update_statuses_updates_existing_enrollment_and_processes_refund(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$studentModel->shouldReceive('find')
|
||||
->once()
|
||||
->with(10)
|
||||
->andReturn([
|
||||
'id' => 10,
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Ali',
|
||||
'parent_id' => 20,
|
||||
]);
|
||||
|
||||
$userModel = Mockery::mock(User::class);
|
||||
$userModel->shouldReceive('find')
|
||||
->once()
|
||||
->with(20)
|
||||
->andReturn([
|
||||
'id' => 20,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'Two',
|
||||
'email' => 'p2@test.com',
|
||||
]);
|
||||
|
||||
DB::shouldReceive('beginTransaction')->once();
|
||||
DB::shouldReceive('commit')->once();
|
||||
DB::shouldReceive('rollBack')->never();
|
||||
|
||||
DB::shouldReceive('table->where->where->first')
|
||||
->once()
|
||||
->andReturn((object) [
|
||||
'student_id' => 10,
|
||||
'parent_id' => 20,
|
||||
'enrollment_status' => 'payment pending',
|
||||
]);
|
||||
|
||||
DB::shouldReceive('table->where->where->update')
|
||||
->once()
|
||||
->andReturn(1);
|
||||
|
||||
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
|
||||
$refundService->shouldReceive('processRefunds')
|
||||
->once()
|
||||
->with([20], '2025-2026', 77)
|
||||
->andReturn([
|
||||
'errors' => [],
|
||||
'refundAmountByParent' => [20 => 55.50],
|
||||
]);
|
||||
|
||||
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
|
||||
$eventService->shouldReceive('dispatchGroupedEvents')
|
||||
->once();
|
||||
|
||||
$service = new AdministratorEnrollmentStatusService(
|
||||
$shared,
|
||||
$studentModel,
|
||||
$userModel,
|
||||
$refundService,
|
||||
$eventService
|
||||
);
|
||||
|
||||
$result = $service->updateStatuses([
|
||||
10 => 'refund pending',
|
||||
], 77);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(200, $result['status']);
|
||||
}
|
||||
|
||||
public function test_update_statuses_returns_partial_success_when_invalid_status_present(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
|
||||
DB::shouldReceive('beginTransaction')->once();
|
||||
DB::shouldReceive('commit')->once();
|
||||
|
||||
$studentModel = Mockery::mock(Student::class);
|
||||
$userModel = Mockery::mock(User::class);
|
||||
|
||||
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
|
||||
$refundService->shouldReceive('processRefunds')
|
||||
->once()
|
||||
->andReturn([
|
||||
'errors' => [],
|
||||
'refundAmountByParent' => [],
|
||||
]);
|
||||
|
||||
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
|
||||
$eventService->shouldReceive('dispatchGroupedEvents')->once();
|
||||
|
||||
$service = new AdministratorEnrollmentStatusService(
|
||||
$shared,
|
||||
$studentModel,
|
||||
$userModel,
|
||||
$refundService,
|
||||
$eventService
|
||||
);
|
||||
|
||||
$result = $service->updateStatuses([
|
||||
10 => 'not-valid',
|
||||
], 77);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(207, $result['status']);
|
||||
$this->assertStringContainsString('Invalid enrollment status', $result['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorMetricsService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorMetricsServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_metrics_returns_counts(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
$service = new AdministratorMetricsService(
|
||||
new \App\Services\Administrator\AdministratorSharedService(new \App\Models\Configuration()),
|
||||
new \App\Models\User(),
|
||||
new \App\Models\LoginActivity()
|
||||
);
|
||||
|
||||
$metrics = $service->metrics();
|
||||
|
||||
$this->assertSame(0, $metrics['counts']['students']);
|
||||
$this->assertSame('2025-2026', $metrics['meta']['schoolYear']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorNotificationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorNotificationServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_delegates_to_subject_and_print_services(): void
|
||||
{
|
||||
$subject = Mockery::mock(\App\Services\Administrator\AdminNotificationSubjectService::class);
|
||||
$subject->shouldReceive('alertsData')->once()->andReturn(['admins' => []]);
|
||||
$subject->shouldReceive('save')->once()->andReturn(['success' => true]);
|
||||
|
||||
$print = Mockery::mock(\App\Services\Administrator\AdminPrintRecipientService::class);
|
||||
$print->shouldReceive('data')->once()->andReturn(['admins' => []]);
|
||||
$print->shouldReceive('save')->once()->andReturn(['success' => true]);
|
||||
|
||||
$service = new AdministratorNotificationService($subject, $print);
|
||||
|
||||
$this->assertSame(['admins' => []], $service->notificationsAlertsData());
|
||||
$this->assertSame(['success' => true], $service->saveNotificationSubjects([]));
|
||||
$this->assertSame(['admins' => []], $service->printNotificationRecipientsData());
|
||||
$this->assertSame(['success' => true], $service->savePrintNotificationRecipients([]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorSharedService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorSharedServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_previous_school_year(): void
|
||||
{
|
||||
$service = new AdministratorSharedService(new \App\Models\Configuration());
|
||||
|
||||
$this->assertSame('2024-2025', $service->getPreviousSchoolYear('2025-2026'));
|
||||
$this->assertSame('2024-25', $service->getPreviousSchoolYear('2025-26'));
|
||||
$this->assertSame('2024', $service->getPreviousSchoolYear('2025'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorTeacherSubmissionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorTeacherSubmissionServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_delegates_to_report_and_notification(): void
|
||||
{
|
||||
$report = Mockery::mock(\App\Services\Administrator\TeacherSubmissionReportService::class);
|
||||
$report->shouldReceive('report')->once()->andReturn(['rows' => []]);
|
||||
|
||||
$notify = Mockery::mock(\App\Services\Administrator\TeacherSubmissionNotificationService::class);
|
||||
$notify->shouldReceive('send')->once()->andReturn(['success' => true]);
|
||||
|
||||
$service = new AdministratorTeacherSubmissionService($report, $notify);
|
||||
|
||||
$this->assertSame(['rows' => []], $service->report());
|
||||
$this->assertSame(['success' => true], $service->sendNotifications(new \Illuminate\Http\Request(), 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorUserSearchService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorUserSearchServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_empty_query_returns_no_results(): void
|
||||
{
|
||||
$service = new AdministratorUserSearchService();
|
||||
$result = $service->search('');
|
||||
|
||||
$this->assertSame('', $result['query']);
|
||||
$this->assertSame([], $result['results']);
|
||||
$this->assertSame(0, $result['total_found']);
|
||||
}
|
||||
}
|
||||
@@ -2,172 +2,27 @@
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\TeacherSubmissionNotificationHistory;
|
||||
use App\Models\User;
|
||||
use App\Services\Administrator\AdministratorSharedService;
|
||||
use App\Services\Administrator\TeacherSubmissionNotificationService;
|
||||
use App\Services\Administrator\TeacherSubmissionSupportService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TeacherSubmissionNotificationServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_send_returns_validation_style_error_when_notify_missing(): void
|
||||
public function test_send_requires_targets(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$support = new TeacherSubmissionSupportService();
|
||||
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
|
||||
$support = Mockery::mock(\App\Services\Administrator\TeacherSubmissionSupportService::class);
|
||||
|
||||
$service = new TeacherSubmissionNotificationService($shared, $support);
|
||||
|
||||
$request = new Request([]);
|
||||
$request = Request::create('/notify', 'POST', []);
|
||||
|
||||
$result = $service->send($request, 1);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
|
||||
public function test_send_sends_email_and_logs_history(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
|
||||
$support = new TeacherSubmissionSupportService();
|
||||
|
||||
$request = new Request([
|
||||
'notify' => [
|
||||
1 => [
|
||||
5 => 1,
|
||||
],
|
||||
],
|
||||
'missing_items' => [
|
||||
1 => [
|
||||
5 => base64_encode(json_encode(['midterm scores', 'attendance'])),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$section = new ClassSection();
|
||||
$section->class_section_id = 1;
|
||||
$section->class_section_name = 'Grade 5A';
|
||||
|
||||
$teacher = new User();
|
||||
$teacher->id = 5;
|
||||
$teacher->firstname = 'John';
|
||||
$teacher->lastname = 'Teacher';
|
||||
$teacher->email = 'teacher@test.com';
|
||||
|
||||
$admin = new User();
|
||||
$admin->id = 1;
|
||||
$admin->firstname = 'Admin';
|
||||
$admin->lastname = 'User';
|
||||
|
||||
ClassSection::shouldReceive('query->select->whereIn->get->keyBy')
|
||||
->once()
|
||||
->andReturn(collect([1 => $section]));
|
||||
|
||||
User::shouldReceive('query->select->whereIn->get->keyBy')
|
||||
->once()
|
||||
->andReturn(collect([5 => $teacher]));
|
||||
|
||||
User::shouldReceive('find')
|
||||
->once()
|
||||
->with(1)
|
||||
->andReturn($admin);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
TeacherSubmissionNotificationHistory::shouldReceive('create')
|
||||
->once()
|
||||
->with(Mockery::on(function ($data) {
|
||||
return $data['teacher_id'] === 5
|
||||
&& $data['class_section_id'] === 1
|
||||
&& $data['admin_id'] === 1
|
||||
&& $data['notification_category'] === 'teacher_submissions'
|
||||
&& $data['status'] === 'sent';
|
||||
}))
|
||||
->andReturnTrue();
|
||||
|
||||
$service = new TeacherSubmissionNotificationService($shared, $support);
|
||||
|
||||
$result = $service->send($request, 1);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(200, $result['status']);
|
||||
$this->assertSame(1, $result['sent']);
|
||||
$this->assertSame(0, $result['failed']);
|
||||
|
||||
Mail::assertSentCount(1);
|
||||
}
|
||||
|
||||
public function test_send_marks_failed_when_teacher_email_invalid(): void
|
||||
{
|
||||
$shared = Mockery::mock(AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
|
||||
$support = new TeacherSubmissionSupportService();
|
||||
|
||||
$request = new Request([
|
||||
'notify' => [
|
||||
1 => [
|
||||
5 => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$section = new ClassSection();
|
||||
$section->class_section_id = 1;
|
||||
$section->class_section_name = 'Grade 5A';
|
||||
|
||||
$teacher = new User();
|
||||
$teacher->id = 5;
|
||||
$teacher->firstname = 'John';
|
||||
$teacher->lastname = 'Teacher';
|
||||
$teacher->email = 'invalid-email';
|
||||
|
||||
$admin = new User();
|
||||
$admin->id = 1;
|
||||
$admin->firstname = 'Admin';
|
||||
$admin->lastname = 'User';
|
||||
|
||||
ClassSection::shouldReceive('query->select->whereIn->get->keyBy')
|
||||
->once()
|
||||
->andReturn(collect([1 => $section]));
|
||||
|
||||
User::shouldReceive('query->select->whereIn->get->keyBy')
|
||||
->once()
|
||||
->andReturn(collect([5 => $teacher]));
|
||||
|
||||
User::shouldReceive('find')
|
||||
->once()
|
||||
->with(1)
|
||||
->andReturn($admin);
|
||||
|
||||
TeacherSubmissionNotificationHistory::shouldReceive('create')
|
||||
->once()
|
||||
->with(Mockery::on(function ($data) {
|
||||
return $data['status'] === 'failed';
|
||||
}))
|
||||
->andReturnTrue();
|
||||
|
||||
$service = new TeacherSubmissionNotificationService($shared, $support);
|
||||
|
||||
$result = $service->send($request, 1);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(207, $result['status']);
|
||||
$this->assertSame(0, $result['sent']);
|
||||
$this->assertSame(1, $result['failed']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\SemesterScore;
|
||||
use App\Models\TeacherSubmissionNotificationHistory;
|
||||
use App\Services\Administrator\AdministratorSharedService;
|
||||
use App\Services\Administrator\TeacherSubmissionReportService;
|
||||
use App\Services\Administrator\TeacherSubmissionSupportService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -19,298 +11,31 @@ class TeacherSubmissionReportServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
public function test_report_returns_summary(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
|
||||
$shared->shouldReceive('getSemester')->andReturn('Fall');
|
||||
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
|
||||
|
||||
$support = Mockery::mock(\App\Services\Administrator\TeacherSubmissionSupportService::class);
|
||||
$support->shouldReceive('submissionStatus')->andReturn([
|
||||
'label' => 'No students',
|
||||
'badge' => 'bg-secondary',
|
||||
'detail' => '',
|
||||
'completed' => true,
|
||||
]);
|
||||
$support->shouldReceive('attendanceStatus')->andReturn([
|
||||
'label' => 'Missing',
|
||||
'badge' => 'bg-danger',
|
||||
'completed' => false,
|
||||
]);
|
||||
$support->shouldReceive('buildMissingItems')->andReturn([]);
|
||||
$support->shouldReceive('teacherRolePriority')->andReturn(1);
|
||||
|
||||
$service = new TeacherSubmissionReportService($shared, $support);
|
||||
$report = $service->report();
|
||||
|
||||
$this->assertArrayHasKey('summary', $report);
|
||||
$this->assertSame('2025-2026', $report['schoolYear']);
|
||||
}
|
||||
|
||||
protected function makeSharedService(string $schoolYear = '2025-2026', string $semester = 'Fall'): AdministratorSharedService
|
||||
{
|
||||
$configuration = Mockery::mock(Configuration::class);
|
||||
$configuration->shouldReceive('getConfig')
|
||||
->with('school_year')
|
||||
->andReturn($schoolYear);
|
||||
$configuration->shouldReceive('getConfig')
|
||||
->with('semester')
|
||||
->andReturn($semester);
|
||||
|
||||
return new AdministratorSharedService($configuration);
|
||||
}
|
||||
|
||||
public function test_report_returns_rows_summary_and_history(): void
|
||||
{
|
||||
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('classSection')->insert([
|
||||
[
|
||||
'class_section_id' => 10,
|
||||
'class_section_name' => 'Grade 5A',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 100,
|
||||
'firstname' => 'Main',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'main@test.com',
|
||||
],
|
||||
[
|
||||
'id' => 101,
|
||||
'firstname' => 'TA',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'ta@test.com',
|
||||
],
|
||||
[
|
||||
'id' => 900,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'One',
|
||||
'email' => 'admin@test.com',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
SemesterScore::unguard();
|
||||
SemesterScore::create([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'midterm_exam_score' => '88',
|
||||
'participation_score' => '10',
|
||||
]);
|
||||
|
||||
SemesterScore::create([
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'midterm_exam_score' => '',
|
||||
'participation_score' => '9',
|
||||
]);
|
||||
|
||||
ScoreComment::unguard();
|
||||
ScoreComment::create([
|
||||
'student_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'score_type' => 'midterm',
|
||||
'comment' => 'Good progress',
|
||||
]);
|
||||
|
||||
ScoreComment::create([
|
||||
'student_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'score_type' => 'ptap',
|
||||
'comment' => 'Great effort',
|
||||
]);
|
||||
|
||||
AttendanceDay::unguard();
|
||||
AttendanceDay::create([
|
||||
'class_section_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => now()->format('Y-m-d'),
|
||||
'status' => 'submitted',
|
||||
]);
|
||||
|
||||
TeacherSubmissionNotificationHistory::unguard();
|
||||
TeacherSubmissionNotificationHistory::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(),
|
||||
]);
|
||||
|
||||
$service = new TeacherSubmissionReportService(
|
||||
$this->makeSharedService(),
|
||||
new TeacherSubmissionSupportService()
|
||||
);
|
||||
|
||||
$result = $service->report();
|
||||
|
||||
$this->assertSame('Fall', $result['semester']);
|
||||
$this->assertSame('2025-2026', $result['schoolYear']);
|
||||
$this->assertCount(1, $result['rows']);
|
||||
|
||||
$row = $result['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']);
|
||||
|
||||
$this->assertSame(2, $row['student_count']);
|
||||
|
||||
$this->assertArrayHasKey(10, $result['notificationHistory']);
|
||||
$this->assertArrayHasKey(100, $result['notificationHistory'][10]);
|
||||
$this->assertSame('Admin One', $result['notificationHistory'][10][100][0]['admin_name']);
|
||||
|
||||
$this->assertSame(5, $result['summary']['total_items']);
|
||||
$this->assertSame(3, $result['summary']['missing_items']);
|
||||
$this->assertSame(2, $result['summary']['submitted_items']);
|
||||
$this->assertSame(40, $result['summary']['submission_percentage']);
|
||||
}
|
||||
|
||||
public function test_report_returns_no_students_status_when_section_has_no_students(): void
|
||||
{
|
||||
DB::table('teacher_class')->insert([
|
||||
[
|
||||
'class_section_id' => 11,
|
||||
'teacher_id' => 200,
|
||||
'position' => 'main',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
[
|
||||
'class_section_id' => 11,
|
||||
'class_section_name' => 'Grade 6A',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 200,
|
||||
'firstname' => 'Solo',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'solo@test.com',
|
||||
],
|
||||
]);
|
||||
|
||||
$service = new TeacherSubmissionReportService(
|
||||
$this->makeSharedService(),
|
||||
new TeacherSubmissionSupportService()
|
||||
);
|
||||
|
||||
$result = $service->report();
|
||||
|
||||
$this->assertCount(1, $result['rows']);
|
||||
|
||||
$row = $result['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_notification_history_is_limited_to_three_entries_per_teacher(): void
|
||||
{
|
||||
DB::table('teacher_class')->insert([
|
||||
[
|
||||
'class_section_id' => 12,
|
||||
'teacher_id' => 300,
|
||||
'position' => 'main',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
[
|
||||
'class_section_id' => 12,
|
||||
'class_section_name' => 'Grade 7A',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 300,
|
||||
'firstname' => 'Hist',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'hist@test.com',
|
||||
],
|
||||
[
|
||||
'id' => 901,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'Two',
|
||||
'email' => 'admin2@test.com',
|
||||
],
|
||||
]);
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
TeacherSubmissionNotificationHistory::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),
|
||||
]);
|
||||
}
|
||||
|
||||
$service = new TeacherSubmissionReportService(
|
||||
$this->makeSharedService(),
|
||||
new TeacherSubmissionSupportService()
|
||||
);
|
||||
|
||||
$result = $service->report();
|
||||
|
||||
$this->assertCount(3, $result['notificationHistory'][12][300]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,125 +3,30 @@
|
||||
namespace Tests\Unit\Services\Administrator;
|
||||
|
||||
use App\Services\Administrator\TeacherSubmissionSupportService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TeacherSubmissionSupportServiceTest extends TestCase
|
||||
{
|
||||
protected TeacherSubmissionSupportService $service;
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
public function test_submission_status_labels(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->service = new TeacherSubmissionSupportService();
|
||||
$service = new TeacherSubmissionSupportService();
|
||||
$status = $service->submissionStatus(3, 5);
|
||||
|
||||
$this->assertSame('Missing', $status['label']);
|
||||
$this->assertFalse($status['completed']);
|
||||
}
|
||||
|
||||
public function test_submission_status_returns_no_students_when_expected_is_zero(): void
|
||||
public function test_build_missing_items(): void
|
||||
{
|
||||
$result = $this->service->submissionStatus(0, 0);
|
||||
|
||||
$this->assertSame('No students', $result['label']);
|
||||
$this->assertSame('bg-secondary', $result['badge']);
|
||||
$this->assertTrue($result['completed']);
|
||||
}
|
||||
|
||||
public function test_submission_status_returns_submitted_when_filled_matches_expected(): void
|
||||
{
|
||||
$result = $this->service->submissionStatus(10, 10);
|
||||
|
||||
$this->assertSame('Submitted', $result['label']);
|
||||
$this->assertSame('bg-success', $result['badge']);
|
||||
$this->assertSame('10/10', $result['detail']);
|
||||
$this->assertTrue($result['completed']);
|
||||
}
|
||||
|
||||
public function test_submission_status_returns_missing_when_not_complete(): void
|
||||
{
|
||||
$result = $this->service->submissionStatus(7, 10);
|
||||
|
||||
$this->assertSame('Missing', $result['label']);
|
||||
$this->assertSame('bg-danger', $result['badge']);
|
||||
$this->assertSame('7/10', $result['detail']);
|
||||
$this->assertFalse($result['completed']);
|
||||
}
|
||||
|
||||
public function test_attendance_status_returns_submitted(): void
|
||||
{
|
||||
$result = $this->service->attendanceStatus(true);
|
||||
|
||||
$this->assertSame('Submitted', $result['label']);
|
||||
$this->assertSame('bg-success', $result['badge']);
|
||||
$this->assertTrue($result['completed']);
|
||||
}
|
||||
|
||||
public function test_attendance_status_returns_missing(): void
|
||||
{
|
||||
$result = $this->service->attendanceStatus(false);
|
||||
|
||||
$this->assertSame('Missing', $result['label']);
|
||||
$this->assertSame('bg-danger', $result['badge']);
|
||||
$this->assertFalse($result['completed']);
|
||||
}
|
||||
|
||||
public function test_build_missing_items_returns_only_incomplete_items(): void
|
||||
{
|
||||
$result = $this->service->buildMissingItems([
|
||||
'midterm_score_status' => ['completed' => false],
|
||||
'midterm_comment_status' => ['completed' => true],
|
||||
'participation_status' => ['completed' => false],
|
||||
'ptap_comment_status' => ['completed' => true],
|
||||
$service = new TeacherSubmissionSupportService();
|
||||
$items = $service->buildMissingItems([
|
||||
'attendance_status' => ['completed' => false],
|
||||
'midterm_score_status' => ['completed' => true],
|
||||
]);
|
||||
|
||||
$this->assertSame([
|
||||
'midterm scores',
|
||||
'participation',
|
||||
'attendance',
|
||||
], $result);
|
||||
$this->assertSame(['attendance'], $items);
|
||||
}
|
||||
|
||||
public function test_teacher_role_priority_orders_main_before_ta_before_other(): void
|
||||
{
|
||||
$this->assertSame(1, $this->service->teacherRolePriority('main'));
|
||||
$this->assertSame(2, $this->service->teacherRolePriority('ta'));
|
||||
$this->assertSame(3, $this->service->teacherRolePriority('teacher'));
|
||||
}
|
||||
|
||||
public function test_truncate_notification_message_strips_tags_and_truncates(): void
|
||||
{
|
||||
$html = '<p>Hello <strong>world</strong></p>';
|
||||
$result = $this->service->truncateNotificationMessage($html, 5);
|
||||
|
||||
$this->assertSame('Hello…', $result);
|
||||
}
|
||||
|
||||
public function test_parse_missing_items_payload_returns_unique_trimmed_values(): void
|
||||
{
|
||||
$payload = base64_encode(json_encode([
|
||||
' midterm scores ',
|
||||
'attendance',
|
||||
'attendance',
|
||||
'',
|
||||
]));
|
||||
|
||||
$result = $this->service->parseMissingItemsPayload($payload);
|
||||
|
||||
$this->assertSame([
|
||||
'midterm scores',
|
||||
'attendance',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function test_format_missing_items_text_formats_lists_correctly(): void
|
||||
{
|
||||
$this->assertSame('', $this->service->formatMissingItemsText([]));
|
||||
$this->assertSame('attendance', $this->service->formatMissingItemsText(['attendance']));
|
||||
$this->assertSame(
|
||||
'attendance and midterm scores',
|
||||
$this->service->formatMissingItemsText(['attendance', 'midterm scores'])
|
||||
);
|
||||
$this->assertSame(
|
||||
'attendance, midterm scores and participation',
|
||||
$this->service->formatMissingItemsText(['attendance', 'midterm scores', 'participation'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Assignment;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Services\Assignment\AssignmentContextService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentContextServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_current_term_values(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
$service = new AssignmentContextService(new Configuration());
|
||||
|
||||
$this->assertSame('Fall', $service->getCurrentSemester());
|
||||
$this->assertSame('2025-2026', $service->getCurrentSchoolYear());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Assignment;
|
||||
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Services\Assignment\AssignmentDataLoaderService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentDataLoaderServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_load_teacher_classes_includes_teacher_name(): void
|
||||
{
|
||||
$this->seedTeacherData();
|
||||
|
||||
$service = new AssignmentDataLoaderService(new TeacherClass(), new StudentClass());
|
||||
$rows = $service->loadTeacherClasses('2025-2026');
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame('Teacher One', $rows->first()->teacher_full_name);
|
||||
}
|
||||
|
||||
public function test_load_student_classes_filters_inactive(): void
|
||||
{
|
||||
$this->seedStudentData();
|
||||
|
||||
$service = new AssignmentDataLoaderService(new TeacherClass(), new StudentClass());
|
||||
$rows = $service->loadStudentClasses('2025-2026');
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame(1, $rows->first()->student_id);
|
||||
}
|
||||
|
||||
private function seedTeacherData(): void
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'teacher_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedStudentData(): void
|
||||
{
|
||||
DB::table('students')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Active',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'school_id' => 'S-2',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Inactive',
|
||||
'age' => 9,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Assignment;
|
||||
|
||||
use App\Services\Assignment\AssignmentMetaService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentMetaServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_school_years_uses_fallback(): void
|
||||
{
|
||||
$service = new AssignmentMetaService();
|
||||
$years = $service->getSchoolYears('2025-2026');
|
||||
|
||||
$this->assertSame(['2025-2026'], $years);
|
||||
}
|
||||
|
||||
public function test_get_school_years_returns_distinct_sorted(): void
|
||||
{
|
||||
DB::table('teacher_class')->insert([
|
||||
['class_section_id' => 101, 'teacher_id' => 1, 'position' => 'main', 'school_year' => '2024-2025'],
|
||||
['class_section_id' => 102, 'teacher_id' => 2, 'position' => 'main', 'school_year' => '2025-2026'],
|
||||
['class_section_id' => 103, 'teacher_id' => 3, 'position' => 'main', 'school_year' => '2025-2026'],
|
||||
]);
|
||||
|
||||
$service = new AssignmentMetaService();
|
||||
$years = $service->getSchoolYears();
|
||||
|
||||
$this->assertSame(['2025-2026', '2024-2025'], $years);
|
||||
}
|
||||
|
||||
public function test_first_non_empty_returns_first_value(): void
|
||||
{
|
||||
$service = new AssignmentMetaService();
|
||||
$value = $service->firstNonEmpty([null, '', 'Fall', 'Spring']);
|
||||
|
||||
$this->assertSame('Fall', $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Assignment;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Services\Assignment\AssignmentSectionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentSectionServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_section_names_map(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new AssignmentSectionService(new ClassSection());
|
||||
$map = $service->getSectionNamesMap([1]);
|
||||
|
||||
$this->assertSame('1-A', $map[1]);
|
||||
}
|
||||
}
|
||||
@@ -3,55 +3,40 @@
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Services\Attendance\AttendancePolicyService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendancePolicyServiceTest extends TestCase
|
||||
{
|
||||
public function test_teacher_can_edit_draft_day(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_teacher_can_edit_draft(): void
|
||||
{
|
||||
$service = new AttendancePolicyService();
|
||||
$allowed = $service->canEditDay(
|
||||
['status' => 'draft'],
|
||||
['roles' => ['Teacher'], 'permissions' => []]
|
||||
);
|
||||
|
||||
$day = ['status' => 'draft'];
|
||||
$user = [
|
||||
'roles' => ['teacher'],
|
||||
'permissions' => [],
|
||||
];
|
||||
|
||||
$this->assertTrue($service->canEditDay($day, $user));
|
||||
$this->assertTrue($allowed);
|
||||
}
|
||||
|
||||
public function test_teacher_cannot_edit_finalized_day(): void
|
||||
public function test_teacher_cannot_edit_finalized(): void
|
||||
{
|
||||
$service = new AttendancePolicyService();
|
||||
$allowed = $service->canEditDay(
|
||||
['status' => 'finalized'],
|
||||
['roles' => ['Teacher'], 'permissions' => []]
|
||||
);
|
||||
|
||||
$day = ['status' => 'finalized'];
|
||||
$user = [
|
||||
'roles' => ['teacher'],
|
||||
'permissions' => [],
|
||||
];
|
||||
|
||||
$this->assertFalse($service->canEditDay($day, $user));
|
||||
$this->assertFalse($allowed);
|
||||
}
|
||||
|
||||
public function test_admin_can_edit_finalized_day(): void
|
||||
public function test_admin_like_by_permission(): void
|
||||
{
|
||||
$service = new AttendancePolicyService();
|
||||
$isAdmin = $service->isAdminLike([], ['attendance.manage.daily']);
|
||||
|
||||
$day = ['status' => 'finalized'];
|
||||
$user = [
|
||||
'roles' => ['admin'],
|
||||
'permissions' => [],
|
||||
];
|
||||
|
||||
$this->assertTrue($service->canEditDay($day, $user));
|
||||
$this->assertTrue($isAdmin);
|
||||
}
|
||||
|
||||
public function test_is_admin_like_excludes_teacher_roles(): void
|
||||
{
|
||||
$service = new AttendancePolicyService();
|
||||
|
||||
$this->assertFalse($service->isAdminLike(['teacher']));
|
||||
$this->assertFalse($service->isAdminLike(['teacher_assistant']));
|
||||
$this->assertTrue($service->isAdminLike(['principal']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Services\Attendance\AttendanceQueryService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceQueryServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_teacher_grid_returns_section_labels_and_grid(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 10,
|
||||
'class_section_name' => '1A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'class_section_id' => 10,
|
||||
'teacher_id' => 1,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('staff_attendance')->insert([
|
||||
'user_id' => 1,
|
||||
'role_name' => 'teacher',
|
||||
'date' => '2025-01-05',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'present',
|
||||
'reason' => null,
|
||||
]);
|
||||
|
||||
$service = new AttendanceQueryService(
|
||||
new \App\Models\Configuration(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\AttendanceDay(),
|
||||
new \App\Models\AttendanceRecord(),
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\StudentClass(),
|
||||
new \App\Models\ClassSection(),
|
||||
new \App\Models\TeacherClass(),
|
||||
new \App\Models\Calendar(),
|
||||
new \App\Models\User(),
|
||||
new \App\Models\UserRole(),
|
||||
new \App\Services\Attendance\AttendanceService(
|
||||
new \App\Models\Configuration(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\AttendanceDay(),
|
||||
new \App\Models\ClassSection(),
|
||||
new \App\Models\UserRole(),
|
||||
new \App\Services\Attendance\AttendancePolicyService(),
|
||||
new \App\Services\Attendance\StudentAttendanceWriterService(
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\Student(),
|
||||
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
|
||||
),
|
||||
new \App\Services\Attendance\TeacherAttendanceSubmissionService(
|
||||
new \App\Models\AttendanceDay(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\TeacherClass(),
|
||||
new \App\Models\Student(),
|
||||
new \App\Services\Attendance\AttendancePolicyService(),
|
||||
new \App\Services\Attendance\StudentAttendanceWriterService(
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\Student(),
|
||||
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
|
||||
)
|
||||
),
|
||||
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
|
||||
),
|
||||
new \App\Services\Attendance\SemesterRangeService()
|
||||
);
|
||||
|
||||
$grid = $service->teacherGrid('Fall', '2025-2026', '2025-01-05', 10);
|
||||
|
||||
$this->assertSame('1A', $grid['sections'][10]);
|
||||
$this->assertSame(1, count($grid['grid']));
|
||||
$this->assertSame('present', $grid['grid'][0]['status']);
|
||||
}
|
||||
}
|
||||
@@ -2,266 +2,46 @@
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceRecord;
|
||||
use App\Services\Attendance\AttendanceRecordSyncService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceRecordSyncServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected AttendanceRecordSyncService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
public function test_update_attendance_record_adjusts_totals(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->service = app(AttendanceRecordSyncService::class);
|
||||
}
|
||||
|
||||
public function test_update_attendance_record_increments_and_decrements_counters(): void
|
||||
{
|
||||
$record = AttendanceRecord::query()->create([
|
||||
'class_section_id' => 10,
|
||||
'student_id' => 200,
|
||||
'school_id' => 'SCH-200',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
DB::table('attendance_record')->insert([
|
||||
'class_section_id' => 1,
|
||||
'student_id' => 10,
|
||||
'school_id' => 'S1',
|
||||
'total_presence' => 5,
|
||||
'total_absence' => 2,
|
||||
'total_late' => 1,
|
||||
'total_absence' => 1,
|
||||
'total_late' => 2,
|
||||
'total_attendance' => 8,
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->service->updateAttendanceRecord(
|
||||
studentId: 200,
|
||||
schoolId: 'SCH-200',
|
||||
newStatus: 'absent',
|
||||
semester: 'Fall',
|
||||
schoolYear: '2025-2026',
|
||||
oldStatus: 'present',
|
||||
modifiedBy: 99
|
||||
);
|
||||
|
||||
$record->refresh();
|
||||
|
||||
$this->assertSame(4, (int)$record->total_presence);
|
||||
$this->assertSame(3, (int)$record->total_absence);
|
||||
$this->assertSame(1, (int)$record->total_late);
|
||||
$this->assertSame(99, (int)$record->modified_by);
|
||||
}
|
||||
|
||||
public function test_update_attendance_record_does_nothing_when_record_missing(): void
|
||||
{
|
||||
$this->service->updateAttendanceRecord(
|
||||
studentId: 999,
|
||||
schoolId: 'SCH-999',
|
||||
newStatus: 'late',
|
||||
semester: 'Fall',
|
||||
schoolYear: '2025-2026',
|
||||
oldStatus: 'present',
|
||||
modifiedBy: 10
|
||||
);
|
||||
|
||||
$this->assertDatabaseMissing('attendance_record', [
|
||||
'student_id' => 999,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new AttendanceRecordSyncService(new \App\Models\AttendanceRecord());
|
||||
$service->updateAttendanceRecord(10, 'S1', 'absent', 'Fall', '2025-2026', 'present', 1);
|
||||
|
||||
$row = DB::table('attendance_record')->where('student_id', 10)->first();
|
||||
$this->assertSame(4, (int) $row->total_presence);
|
||||
$this->assertSame(2, (int) $row->total_absence);
|
||||
}
|
||||
|
||||
public function test_update_attendance_record_never_goes_below_zero(): void
|
||||
public function test_add_new_attendance_record_creates_row(): void
|
||||
{
|
||||
$record = AttendanceRecord::query()->create([
|
||||
'class_section_id' => 11,
|
||||
'student_id' => 201,
|
||||
'school_id' => 'SCH-201',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 0,
|
||||
'total_absence' => 0,
|
||||
'total_late' => 0,
|
||||
'total_attendance' => 0,
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->service->updateAttendanceRecord(
|
||||
studentId: 201,
|
||||
schoolId: 'SCH-201',
|
||||
newStatus: 'present',
|
||||
semester: 'Fall',
|
||||
schoolYear: '2025-2026',
|
||||
oldStatus: 'absent',
|
||||
modifiedBy: 5
|
||||
);
|
||||
|
||||
$record->refresh();
|
||||
|
||||
$this->assertSame(1, (int)$record->total_presence);
|
||||
$this->assertSame(0, (int)$record->total_absence);
|
||||
$this->assertSame(0, (int)$record->total_late);
|
||||
}
|
||||
|
||||
public function test_add_new_attendance_record_creates_record_when_missing(): void
|
||||
{
|
||||
$this->service->addNewAttendanceRecord(
|
||||
classSectionId: 20,
|
||||
studentId: 300,
|
||||
schoolId: 'SCH-300',
|
||||
status: 'late',
|
||||
semester: 'Spring',
|
||||
schoolYear: '2025-2026',
|
||||
modifiedBy: 88
|
||||
);
|
||||
$service = new AttendanceRecordSyncService(new \App\Models\AttendanceRecord());
|
||||
$service->addNewAttendanceRecord(1, 10, 'S1', 'present', 'Fall', '2025-2026', 1);
|
||||
|
||||
$this->assertDatabaseHas('attendance_record', [
|
||||
'class_section_id' => 20,
|
||||
'student_id' => 300,
|
||||
'school_id' => 'SCH-300',
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 0,
|
||||
'total_absence' => 0,
|
||||
'total_late' => 1,
|
||||
'total_attendance' => 1,
|
||||
'modified_by' => 88,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_add_new_attendance_record_updates_existing_record(): void
|
||||
{
|
||||
$record = AttendanceRecord::query()->create([
|
||||
'class_section_id' => 21,
|
||||
'student_id' => 301,
|
||||
'school_id' => 'SCH-301',
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
'student_id' => 10,
|
||||
'total_presence' => 1,
|
||||
'total_absence' => 2,
|
||||
'total_late' => 3,
|
||||
'total_attendance' => 6,
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->service->addNewAttendanceRecord(
|
||||
classSectionId: 21,
|
||||
studentId: 301,
|
||||
schoolId: 'SCH-301',
|
||||
status: 'present',
|
||||
semester: 'Spring',
|
||||
schoolYear: '2025-2026',
|
||||
modifiedBy: 77
|
||||
);
|
||||
|
||||
$record->refresh();
|
||||
|
||||
$this->assertSame(2, (int)$record->total_presence);
|
||||
$this->assertSame(2, (int)$record->total_absence);
|
||||
$this->assertSame(3, (int)$record->total_late);
|
||||
$this->assertSame(7, (int)$record->total_attendance);
|
||||
$this->assertSame(77, (int)$record->modified_by);
|
||||
}
|
||||
|
||||
public function test_upsert_attendance_record_totals_updates_existing_record(): void
|
||||
{
|
||||
$record = AttendanceRecord::query()->create([
|
||||
'class_section_id' => 30,
|
||||
'student_id' => 400,
|
||||
'school_id' => 'SCH-400',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 0,
|
||||
'total_absence' => 0,
|
||||
'total_late' => 0,
|
||||
'total_attendance' => 0,
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $this->service->upsertAttendanceRecordTotals(
|
||||
studentId: 400,
|
||||
classSectionId: '30',
|
||||
schoolId: 'SCH-400',
|
||||
semester: 'Fall',
|
||||
schoolYear: '2025-2026',
|
||||
totalPresence: 7,
|
||||
totalLate: 2,
|
||||
totalAbsence: 1,
|
||||
totalAttendance: 10,
|
||||
modifiedBy: 44
|
||||
);
|
||||
|
||||
$this->assertTrue($result);
|
||||
|
||||
$record->refresh();
|
||||
|
||||
$this->assertSame(7, (int)$record->total_presence);
|
||||
$this->assertSame(2, (int)$record->total_late);
|
||||
$this->assertSame(1, (int)$record->total_absence);
|
||||
$this->assertSame(10, (int)$record->total_attendance);
|
||||
$this->assertSame(44, (int)$record->modified_by);
|
||||
}
|
||||
|
||||
public function test_upsert_attendance_record_totals_creates_new_record(): void
|
||||
{
|
||||
$result = $this->service->upsertAttendanceRecordTotals(
|
||||
studentId: 401,
|
||||
classSectionId: '31',
|
||||
schoolId: 'SCH-401',
|
||||
semester: 'Fall',
|
||||
schoolYear: '2025-2026',
|
||||
totalPresence: 3,
|
||||
totalLate: 1,
|
||||
totalAbsence: 2,
|
||||
totalAttendance: 6,
|
||||
modifiedBy: 55
|
||||
);
|
||||
|
||||
$this->assertTrue($result);
|
||||
|
||||
$this->assertDatabaseHas('attendance_record', [
|
||||
'student_id' => 401,
|
||||
'class_section_id' => 31,
|
||||
'school_id' => 'SCH-401',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 3,
|
||||
'total_late' => 1,
|
||||
'total_absence' => 2,
|
||||
'total_attendance' => 6,
|
||||
'modified_by' => 55,
|
||||
'total_attendance' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_upsert_attendance_record_totals_returns_false_when_semester_or_school_year_missing(): void
|
||||
{
|
||||
$result = $this->service->upsertAttendanceRecordTotals(
|
||||
studentId: 402,
|
||||
classSectionId: '32',
|
||||
schoolId: 'SCH-402',
|
||||
semester: '',
|
||||
schoolYear: '',
|
||||
totalPresence: 1,
|
||||
totalLate: 1,
|
||||
totalAbsence: 1,
|
||||
totalAttendance: 3,
|
||||
modifiedBy: 1
|
||||
);
|
||||
|
||||
$this->assertFalse($result);
|
||||
|
||||
$this->assertDatabaseMissing('attendance_record', [
|
||||
'student_id' => 402,
|
||||
'school_id' => 'SCH-402',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Services\Attendance\SemesterRangeService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceSemesterRangeServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_semester_range(): void
|
||||
{
|
||||
$service = new SemesterRangeService();
|
||||
$range = $service->getSemesterRange('2025-2026', 'fall');
|
||||
|
||||
$this->assertSame(['2025-09-21', '2026-01-18'], $range);
|
||||
}
|
||||
|
||||
public function test_build_sunday_list_returns_sundays(): void
|
||||
{
|
||||
$service = new SemesterRangeService();
|
||||
$dates = $service->buildSundayList('2025-09-01', '2025-09-30');
|
||||
|
||||
$this->assertContains('2025-09-07', $dates);
|
||||
$this->assertContains('2025-09-14', $dates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use App\Services\Attendance\AttendancePolicyService;
|
||||
use App\Services\Attendance\AttendanceRecordSyncService;
|
||||
use App\Services\Attendance\AttendanceService;
|
||||
use App\Services\Attendance\StudentAttendanceWriterService;
|
||||
use App\Services\Attendance\TeacherAttendanceSubmissionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_update_attendance_management_throws_when_locked(): void
|
||||
{
|
||||
$user = $this->seedUser();
|
||||
|
||||
$policy = Mockery::mock(AttendancePolicyService::class);
|
||||
$policy->shouldReceive('canEditDay')->andReturn(false);
|
||||
|
||||
$writer = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
$teacherSubmission = Mockery::mock(TeacherAttendanceSubmissionService::class);
|
||||
$recordSync = Mockery::mock(AttendanceRecordSyncService::class);
|
||||
|
||||
$service = new AttendanceService(
|
||||
new Configuration(),
|
||||
new AttendanceData(),
|
||||
new AttendanceDay(),
|
||||
new ClassSection(),
|
||||
new UserRole(),
|
||||
$policy,
|
||||
$writer,
|
||||
$teacherSubmission,
|
||||
$recordSync
|
||||
);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$service->updateAttendanceManagement($user, [
|
||||
'class_section_id' => 1,
|
||||
'student_id' => 1,
|
||||
'status' => 'present',
|
||||
'date' => '2025-01-01',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_attendance_management_saves_row(): void
|
||||
{
|
||||
$user = $this->seedUser();
|
||||
|
||||
$policy = Mockery::mock(AttendancePolicyService::class);
|
||||
$policy->shouldReceive('canEditDay')->andReturn(true);
|
||||
|
||||
$writer = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
$writer->shouldReceive('saveOrUpdateStudentAttendance')->once();
|
||||
|
||||
$teacherSubmission = Mockery::mock(TeacherAttendanceSubmissionService::class);
|
||||
$recordSync = Mockery::mock(AttendanceRecordSyncService::class);
|
||||
|
||||
$service = new AttendanceService(
|
||||
new Configuration(),
|
||||
new AttendanceData(),
|
||||
new AttendanceDay(),
|
||||
new ClassSection(),
|
||||
new UserRole(),
|
||||
$policy,
|
||||
$writer,
|
||||
$teacherSubmission,
|
||||
$recordSync
|
||||
);
|
||||
|
||||
$result = $service->updateAttendanceManagement($user, [
|
||||
'class_section_id' => 1,
|
||||
'student_id' => 1,
|
||||
'status' => 'present',
|
||||
'date' => '2025-01-01',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'class_id' => 1,
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertDatabaseHas('attendance_day', [
|
||||
'class_section_id' => 1,
|
||||
'date' => '2025-01-01',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'test@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Services\Attendance\StaffAttendanceService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StaffAttendanceServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_month_data_returns_sections_and_days(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 10,
|
||||
'class_section_name' => '1A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'class_section_id' => 10,
|
||||
'teacher_id' => 1,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('staff_attendance')->insert([
|
||||
'user_id' => 1,
|
||||
'role_name' => 'teacher',
|
||||
'date' => '2025-09-07',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'present',
|
||||
'reason' => null,
|
||||
]);
|
||||
|
||||
DB::table('calendar_events')->insert([
|
||||
'title' => 'No School',
|
||||
'date' => '2025-09-14',
|
||||
'no_school' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new StaffAttendanceService(
|
||||
new \App\Models\Configuration(),
|
||||
new \App\Models\StaffAttendance(),
|
||||
new \App\Models\TeacherClass(),
|
||||
new \App\Models\ClassSection(),
|
||||
new \App\Services\Attendance\SemesterRangeService()
|
||||
);
|
||||
|
||||
$payload = $service->monthData('Fall', '2025-2026');
|
||||
|
||||
$this->assertNotEmpty($payload['sections']);
|
||||
$this->assertContains('2025-09-14', $payload['noSchoolDays']);
|
||||
}
|
||||
}
|
||||
@@ -2,321 +2,90 @@
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceRecord;
|
||||
use App\Models\Student;
|
||||
use App\Services\Attendance\AttendanceRecordSyncService;
|
||||
use App\Services\Attendance\StudentAttendanceWriterService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use RuntimeException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StudentAttendanceWriterServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected StudentAttendanceWriterService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
public function test_resolve_student_school_id_from_student(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->service = app(StudentAttendanceWriterService::class);
|
||||
}
|
||||
|
||||
public function test_resolve_student_school_id_returns_passed_value_when_present(): void
|
||||
{
|
||||
$result = $this->service->resolveStudentSchoolId(100, 'SCH-100');
|
||||
|
||||
$this->assertSame('SCH-100', $result);
|
||||
}
|
||||
|
||||
public function test_resolve_student_school_id_fetches_from_database_when_missing(): void
|
||||
{
|
||||
Student::query()->create([
|
||||
'id' => 101,
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'Doe',
|
||||
'school_id' => 'SCH-101',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
DB::table('students')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'Student',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$result = $this->service->resolveStudentSchoolId(101, null);
|
||||
$sync = Mockery::mock(AttendanceRecordSyncService::class);
|
||||
$service = new StudentAttendanceWriterService(new \App\Models\AttendanceData(), new \App\Models\Student(), $sync);
|
||||
|
||||
$this->assertSame('SCH-101', $result);
|
||||
$this->assertSame('S1', $service->resolveStudentSchoolId(1, null));
|
||||
}
|
||||
|
||||
public function test_resolve_student_school_id_throws_when_student_missing(): void
|
||||
public function test_save_or_update_updates_existing_and_syncs_record(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Student school ID not found.');
|
||||
DB::table('students')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'firstname' => 'Test',
|
||||
'lastname' => 'Student',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$this->service->resolveStudentSchoolId(9999, null);
|
||||
}
|
||||
DB::table('attendance_data')->insert([
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'student_id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'date' => '2025-01-01',
|
||||
'status' => 'present',
|
||||
'is_reported' => 'no',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
public function test_insert_attendance_data_creates_new_row(): void
|
||||
{
|
||||
$this->service->insertAttendanceData(
|
||||
classSectionId: 10,
|
||||
studentId: 200,
|
||||
studentSchoolId: 'SCH-200',
|
||||
status: 'Absent',
|
||||
reason: 'Sick',
|
||||
reported: 'yes',
|
||||
semester: 'Fall',
|
||||
schoolYear: '2025-2026',
|
||||
date: '2025-10-05',
|
||||
classId: 5,
|
||||
userId: 22
|
||||
);
|
||||
$sync = Mockery::mock(AttendanceRecordSyncService::class);
|
||||
$sync->shouldReceive('updateAttendanceRecord')->once();
|
||||
|
||||
$this->assertDatabaseHas('attendance_data', [
|
||||
'class_id' => 5,
|
||||
'class_section_id' => 10,
|
||||
'student_id' => 200,
|
||||
'school_id' => 'SCH-200',
|
||||
$service = new StudentAttendanceWriterService(new \App\Models\AttendanceData(), new \App\Models\Student(), $sync);
|
||||
|
||||
$service->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => 1,
|
||||
'student_id' => 1,
|
||||
'school_id' => null,
|
||||
'status' => 'absent',
|
||||
'reason' => 'Sick',
|
||||
'is_reported' => 'yes',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-05',
|
||||
'modified_by' => 22,
|
||||
'date' => '2025-01-01',
|
||||
'class_id' => 1,
|
||||
'user_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_attendance_data_updates_existing_row(): void
|
||||
{
|
||||
$row = AttendanceData::query()->create([
|
||||
'class_id' => 6,
|
||||
'class_section_id' => 12,
|
||||
'student_id' => 201,
|
||||
'school_id' => 'SCH-201',
|
||||
'status' => 'present',
|
||||
'reason' => null,
|
||||
'is_reported' => 'no',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-12',
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->service->updateAttendanceData(
|
||||
attendanceId: $row->id,
|
||||
newStatus: 'late',
|
||||
newReason: 'Traffic',
|
||||
reported: 'yes',
|
||||
userId: 99
|
||||
);
|
||||
|
||||
$row->refresh();
|
||||
|
||||
$this->assertSame('late', $row->status);
|
||||
$this->assertSame('Traffic', $row->reason);
|
||||
$this->assertSame('yes', $row->is_reported);
|
||||
$this->assertSame(99, (int)$row->modified_by);
|
||||
}
|
||||
|
||||
public function test_save_or_update_student_attendance_creates_new_attendance_and_record(): void
|
||||
{
|
||||
Student::query()->create([
|
||||
'id' => 300,
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Smith',
|
||||
'school_id' => 'SCH-300',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $this->service->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => 15,
|
||||
'student_id' => 300,
|
||||
'school_id' => null,
|
||||
'status' => 'present',
|
||||
'reason' => 'On time',
|
||||
'is_reported' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-19',
|
||||
'class_id' => 9,
|
||||
'user_id' => 88,
|
||||
]);
|
||||
|
||||
$this->assertSame('created', $result['mode']);
|
||||
$this->assertSame('present', $result['new_status']);
|
||||
$this->assertSame('SCH-300', $result['school_id']);
|
||||
|
||||
$this->assertDatabaseHas('attendance_data', [
|
||||
'student_id' => 300,
|
||||
'school_id' => 'SCH-300',
|
||||
'status' => 'present',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-19',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('attendance_record', [
|
||||
'student_id' => 300,
|
||||
'school_id' => 'SCH-300',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 1,
|
||||
'total_absence' => 0,
|
||||
'total_late' => 0,
|
||||
'total_attendance' => 1,
|
||||
'modified_by' => 88,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_save_or_update_student_attendance_updates_existing_attendance_and_record(): void
|
||||
{
|
||||
Student::query()->create([
|
||||
'id' => 301,
|
||||
'firstname' => 'Ali',
|
||||
'lastname' => 'Khan',
|
||||
'school_id' => 'SCH-301',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
AttendanceRecord::query()->create([
|
||||
'class_section_id' => 16,
|
||||
'student_id' => 301,
|
||||
'school_id' => 'SCH-301',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 2,
|
||||
'total_absence' => 0,
|
||||
'total_late' => 0,
|
||||
'total_attendance' => 2,
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$attendance = AttendanceData::query()->create([
|
||||
'class_id' => 10,
|
||||
'class_section_id' => 16,
|
||||
'student_id' => 301,
|
||||
'school_id' => 'SCH-301',
|
||||
'status' => 'present',
|
||||
'reason' => null,
|
||||
'is_reported' => 'no',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-26',
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $this->service->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => 16,
|
||||
'student_id' => 301,
|
||||
'school_id' => 'SCH-301',
|
||||
'student_id' => 1,
|
||||
'status' => 'absent',
|
||||
'reason' => 'Family trip',
|
||||
'reason' => 'Sick',
|
||||
'is_reported' => 'yes',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-26',
|
||||
'class_id' => 10,
|
||||
'user_id' => 77,
|
||||
]);
|
||||
|
||||
$this->assertSame('updated', $result['mode']);
|
||||
$this->assertSame('present', $result['old_status']);
|
||||
$this->assertSame('absent', $result['new_status']);
|
||||
|
||||
$attendance->refresh();
|
||||
|
||||
$this->assertSame('absent', $attendance->status);
|
||||
$this->assertSame('Family trip', $attendance->reason);
|
||||
$this->assertSame('yes', $attendance->is_reported);
|
||||
$this->assertSame(77, (int)$attendance->modified_by);
|
||||
|
||||
$this->assertDatabaseHas('attendance_record', [
|
||||
'student_id' => 301,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 1,
|
||||
'total_absence' => 1,
|
||||
'total_late' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_save_or_update_student_attendance_updates_existing_without_counter_change_when_status_same(): void
|
||||
{
|
||||
Student::query()->create([
|
||||
'id' => 302,
|
||||
'firstname' => 'Mona',
|
||||
'lastname' => 'Lee',
|
||||
'school_id' => 'SCH-302',
|
||||
'is_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$record = AttendanceRecord::query()->create([
|
||||
'class_section_id' => 17,
|
||||
'student_id' => 302,
|
||||
'school_id' => 'SCH-302',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'total_presence' => 4,
|
||||
'total_absence' => 1,
|
||||
'total_late' => 0,
|
||||
'total_attendance' => 5,
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
AttendanceData::query()->create([
|
||||
'class_id' => 11,
|
||||
'class_section_id' => 17,
|
||||
'student_id' => 302,
|
||||
'school_id' => 'SCH-302',
|
||||
'status' => 'present',
|
||||
'reason' => null,
|
||||
'is_reported' => 'no',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-11-02',
|
||||
'modified_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->service->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => 17,
|
||||
'student_id' => 302,
|
||||
'school_id' => 'SCH-302',
|
||||
'status' => 'present',
|
||||
'reason' => 'Still present',
|
||||
'is_reported' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-11-02',
|
||||
'class_id' => 11,
|
||||
'user_id' => 66,
|
||||
]);
|
||||
|
||||
$record->refresh();
|
||||
|
||||
$this->assertSame(4, (int)$record->total_presence);
|
||||
$this->assertSame(1, (int)$record->total_absence);
|
||||
$this->assertSame(0, (int)$record->total_late);
|
||||
$this->assertSame(5, (int)$record->total_attendance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,544 +2,80 @@
|
||||
|
||||
namespace Tests\Unit\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\Student;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use App\Services\Attendance\AttendancePolicyService;
|
||||
use App\Services\Attendance\StudentAttendanceWriterService;
|
||||
use App\Services\Attendance\TeacherAttendanceSubmissionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TeacherAttendanceSubmissionServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function makeService(
|
||||
?AttendanceDay $attendanceDay = null,
|
||||
?AttendanceData $attendanceData = null,
|
||||
mixed $teacherClass = null,
|
||||
?Student $student = null,
|
||||
mixed $attendancePolicyService = null,
|
||||
mixed $studentAttendanceWriterService = null
|
||||
): TeacherAttendanceSubmissionService {
|
||||
return new TeacherAttendanceSubmissionService(
|
||||
$attendanceDay ?? app(AttendanceDay::class),
|
||||
$attendanceData ?? app(AttendanceData::class),
|
||||
$teacherClass ?? app(TeacherClass::class),
|
||||
$student ?? app(Student::class),
|
||||
$attendancePolicyService ?? app(AttendancePolicyService::class),
|
||||
$studentAttendanceWriterService ?? app(StudentAttendanceWriterService::class),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_throws_when_class_section_missing(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Missing or invalid class section.');
|
||||
|
||||
$service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '',
|
||||
'attendance' => [],
|
||||
'teachers' => [],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_throws_when_section_not_found(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Invalid class section.');
|
||||
|
||||
$service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '999999',
|
||||
'attendance' => [['student_id' => 1, 'status' => 'present']],
|
||||
'teachers' => [1 => ['status' => 'present']],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_throws_when_student_attendance_missing(): void
|
||||
public function test_submit_requires_attendance_data(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 5,
|
||||
'class_section_id' => 100,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 10,
|
||||
'class_section_name' => '1A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$teacherClassMock = Mockery::mock(TeacherClass::class);
|
||||
$teacherClassMock->shouldReceive('assignedForSectionTerm')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'teacher_id' => 55,
|
||||
'position' => 'main',
|
||||
'firstname' => 'Main',
|
||||
'lastname' => 'Teacher',
|
||||
],
|
||||
]);
|
||||
$user = $this->seedUser();
|
||||
|
||||
$policyMock = Mockery::mock(AttendancePolicyService::class);
|
||||
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
|
||||
$policy = Mockery::mock(\App\Services\Attendance\AttendancePolicyService::class);
|
||||
$policy->shouldReceive('canEditDay')->andReturn(true);
|
||||
|
||||
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
|
||||
$service = $this->makeService(
|
||||
teacherClass: $teacherClassMock,
|
||||
attendancePolicyService: $policyMock,
|
||||
studentAttendanceWriterService: $writerMock
|
||||
$service = new TeacherAttendanceSubmissionService(
|
||||
new \App\Models\AttendanceDay(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\TeacherClass(),
|
||||
new \App\Models\Student(),
|
||||
$policy,
|
||||
new \App\Services\Attendance\StudentAttendanceWriterService(
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\Student(),
|
||||
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
|
||||
)
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('No student attendance data provided.');
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '100',
|
||||
'attendance' => [],
|
||||
'teachers' => [55 => ['status' => 'present']],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
$user,
|
||||
['class_section_id' => 10],
|
||||
fn () => false,
|
||||
fn () => 'Fall',
|
||||
fn () => '2025-2026'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_throws_when_teacher_attendance_missing(): void
|
||||
private function seedUser(): User
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 2,
|
||||
'class_id' => 6,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
|
||||
$service = $this->makeService();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('No teacher attendance data provided.');
|
||||
|
||||
$service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '101',
|
||||
'attendance' => [
|
||||
['student_id' => 1, 'status' => 'present'],
|
||||
],
|
||||
'teachers' => [],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_throws_when_policy_denies_edit(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 3,
|
||||
'class_id' => 7,
|
||||
'class_section_id' => 102,
|
||||
]);
|
||||
|
||||
$teacherClassMock = Mockery::mock(TeacherClass::class);
|
||||
$teacherClassMock->shouldReceive('assignedForSectionTerm')->never();
|
||||
|
||||
$policyMock = Mockery::mock(AttendancePolicyService::class);
|
||||
$policyMock->shouldReceive('canEditDay')->once()->andReturnFalse();
|
||||
|
||||
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
|
||||
$service = $this->makeService(
|
||||
teacherClass: $teacherClassMock,
|
||||
attendancePolicyService: $policyMock,
|
||||
studentAttendanceWriterService: $writerMock
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Attendance is locked for your role.');
|
||||
|
||||
$service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '102',
|
||||
'attendance' => [
|
||||
['student_id' => 1, 'status' => 'present'],
|
||||
],
|
||||
'teachers' => [
|
||||
55 => ['status' => 'present'],
|
||||
],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_throws_when_assigned_teacher_status_missing_or_invalid(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 4,
|
||||
'class_id' => 8,
|
||||
'class_section_id' => 103,
|
||||
]);
|
||||
|
||||
$teacherClassMock = Mockery::mock(TeacherClass::class);
|
||||
$teacherClassMock->shouldReceive('assignedForSectionTerm')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'teacher_id' => 77,
|
||||
'position' => 'main',
|
||||
'firstname' => 'Main',
|
||||
'lastname' => 'Teacher',
|
||||
],
|
||||
]);
|
||||
|
||||
$policyMock = Mockery::mock(AttendancePolicyService::class);
|
||||
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
|
||||
|
||||
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
|
||||
$service = $this->makeService(
|
||||
teacherClass: $teacherClassMock,
|
||||
attendancePolicyService: $policyMock,
|
||||
studentAttendanceWriterService: $writerMock
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Please fill teacher attendance for all assigned teachers.');
|
||||
|
||||
$service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '103',
|
||||
'attendance' => [
|
||||
['student_id' => 1, 'status' => 'present'],
|
||||
],
|
||||
'teachers' => [
|
||||
77 => ['status' => ''],
|
||||
],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_submit_saves_teacher_staff_attendance_and_student_rows(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 5,
|
||||
'class_id' => 9,
|
||||
'class_section_id' => 104,
|
||||
]);
|
||||
|
||||
$teacherClassMock = Mockery::mock(TeacherClass::class);
|
||||
$teacherClassMock->shouldReceive('assignedForSectionTerm')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'teacher_id' => 88,
|
||||
'position' => 'main',
|
||||
'firstname' => 'Lead',
|
||||
'lastname' => 'Teacher',
|
||||
],
|
||||
[
|
||||
'teacher_id' => 89,
|
||||
'position' => 'ta',
|
||||
'firstname' => 'Assist',
|
||||
'lastname' => 'Teacher',
|
||||
],
|
||||
]);
|
||||
|
||||
$policyMock = Mockery::mock(AttendancePolicyService::class);
|
||||
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
|
||||
$policyMock->shouldReceive('isTeacher')->once()->andReturnTrue();
|
||||
|
||||
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
$writerMock->shouldReceive('saveOrUpdateStudentAttendance')
|
||||
->twice()
|
||||
->withArgs(function (array $payload) {
|
||||
return in_array($payload['student_id'], [501, 502], true)
|
||||
&& $payload['class_section_id'] === 104
|
||||
&& $payload['class_id'] === 9
|
||||
&& $payload['semester'] === 'Fall'
|
||||
&& $payload['school_year'] === '2025-2026';
|
||||
})
|
||||
->andReturn(['mode' => 'created']);
|
||||
|
||||
$service = $this->makeService(
|
||||
teacherClass: $teacherClassMock,
|
||||
attendancePolicyService: $policyMock,
|
||||
studentAttendanceWriterService: $writerMock
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$result = $service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '104',
|
||||
'date' => '2025-10-05',
|
||||
'attendance' => [
|
||||
[
|
||||
'student_id' => 501,
|
||||
'school_id' => 'SCH-501',
|
||||
'status' => 'present',
|
||||
'reason' => '',
|
||||
],
|
||||
[
|
||||
'student_id' => 502,
|
||||
'school_id' => 'SCH-502',
|
||||
'status' => 'late',
|
||||
'reason' => 'Bus delay',
|
||||
],
|
||||
],
|
||||
'teachers' => [
|
||||
88 => ['status' => 'present', 'reason' => null],
|
||||
89 => ['status' => 'late', 'reason' => 'Traffic'],
|
||||
],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
|
||||
$this->assertSame(['ok' => true, 'message' => 'Attendance submitted successfully.'], $result);
|
||||
|
||||
$this->assertDatabaseHas('staff_attendance', [
|
||||
'user_id' => 88,
|
||||
'date' => '2025-10-05',
|
||||
DB::table('users')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'class_section_id' => 104,
|
||||
'position' => 'main',
|
||||
'status' => 'present',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('staff_attendance', [
|
||||
'user_id' => 89,
|
||||
'date' => '2025-10-05',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'class_section_id' => 104,
|
||||
'position' => 'ta',
|
||||
'status' => 'late',
|
||||
'reason' => 'Traffic',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('attendance_day', [
|
||||
'class_section_id' => 104,
|
||||
'date' => '2025-10-05',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'submitted',
|
||||
'submitted_by' => $user->id,
|
||||
]);
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
|
||||
public function test_submit_skips_locked_existing_student_rows_for_teacher(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 6,
|
||||
'class_id' => 10,
|
||||
'class_section_id' => 105,
|
||||
]);
|
||||
|
||||
AttendanceData::query()->create([
|
||||
'class_id' => 10,
|
||||
'class_section_id' => 105,
|
||||
'student_id' => 601,
|
||||
'school_id' => 'SCH-601',
|
||||
'status' => 'absent',
|
||||
'reason' => 'Parent reported',
|
||||
'is_reported' => 'yes',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-12',
|
||||
'modified_by' => 2,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$teacherClassMock = Mockery::mock(TeacherClass::class);
|
||||
$teacherClassMock->shouldReceive('assignedForSectionTerm')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'teacher_id' => 90,
|
||||
'position' => 'main',
|
||||
'firstname' => 'Lead',
|
||||
'lastname' => 'Teacher',
|
||||
],
|
||||
]);
|
||||
|
||||
$policyMock = Mockery::mock(AttendancePolicyService::class);
|
||||
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
|
||||
$policyMock->shouldReceive('isTeacher')->once()->andReturnTrue();
|
||||
|
||||
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
$writerMock->shouldNotReceive('saveOrUpdateStudentAttendance');
|
||||
|
||||
$service = $this->makeService(
|
||||
teacherClass: $teacherClassMock,
|
||||
attendancePolicyService: $policyMock,
|
||||
studentAttendanceWriterService: $writerMock
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$result = $service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '105',
|
||||
'date' => '2025-10-12',
|
||||
'attendance' => [
|
||||
[
|
||||
'student_id' => 601,
|
||||
'school_id' => 'SCH-601',
|
||||
'status' => 'present',
|
||||
'reason' => '',
|
||||
],
|
||||
],
|
||||
'teachers' => [
|
||||
90 => ['status' => 'present', 'reason' => null],
|
||||
],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => true, 'source' => 'parent'],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
|
||||
$this->assertDatabaseHas('attendance_day', [
|
||||
'class_section_id' => 105,
|
||||
'date' => '2025-10-12',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'submitted',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_submit_allows_non_teacher_context_to_write_existing_rows(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 7,
|
||||
'class_id' => 11,
|
||||
'class_section_id' => 106,
|
||||
]);
|
||||
|
||||
AttendanceData::query()->create([
|
||||
'class_id' => 11,
|
||||
'class_section_id' => 106,
|
||||
'student_id' => 701,
|
||||
'school_id' => 'SCH-701',
|
||||
'status' => 'absent',
|
||||
'reason' => 'Parent reported',
|
||||
'is_reported' => 'yes',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'date' => '2025-10-19',
|
||||
'modified_by' => 2,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$teacherClassMock = Mockery::mock(TeacherClass::class);
|
||||
$teacherClassMock->shouldReceive('assignedForSectionTerm')
|
||||
->once()
|
||||
->andReturn([
|
||||
[
|
||||
'teacher_id' => 91,
|
||||
'position' => 'main',
|
||||
'firstname' => 'Lead',
|
||||
'lastname' => 'Teacher',
|
||||
],
|
||||
]);
|
||||
|
||||
$policyMock = Mockery::mock(AttendancePolicyService::class);
|
||||
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
|
||||
$policyMock->shouldReceive('isTeacher')->once()->andReturnFalse();
|
||||
|
||||
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
|
||||
$writerMock->shouldReceive('saveOrUpdateStudentAttendance')
|
||||
->once()
|
||||
->withArgs(function (array $payload) {
|
||||
return $payload['student_id'] === 701
|
||||
&& $payload['class_section_id'] === 106
|
||||
&& $payload['class_id'] === 11;
|
||||
})
|
||||
->andReturn(['mode' => 'updated']);
|
||||
|
||||
$service = $this->makeService(
|
||||
teacherClass: $teacherClassMock,
|
||||
attendancePolicyService: $policyMock,
|
||||
studentAttendanceWriterService: $writerMock
|
||||
);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$result = $service->submit(
|
||||
user: $user,
|
||||
data: [
|
||||
'class_section_id' => '106',
|
||||
'date' => '2025-10-19',
|
||||
'attendance' => [
|
||||
[
|
||||
'student_id' => 701,
|
||||
'school_id' => 'SCH-701',
|
||||
'status' => 'present',
|
||||
'reason' => '',
|
||||
],
|
||||
],
|
||||
'teachers' => [
|
||||
91 => ['status' => 'present', 'reason' => null],
|
||||
],
|
||||
],
|
||||
detectExternalSubmission: fn (?array $row) => ['locked' => true, 'source' => 'parent'],
|
||||
currentSemester: fn () => 'Fall',
|
||||
currentSchoolYear: fn () => '2025-2026'
|
||||
);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,44 +2,28 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceTracking;
|
||||
use App\Models\Student;
|
||||
use App\Services\AttendanceCaseQueryService;
|
||||
use App\Services\AttendanceParentLookupService;
|
||||
use App\Services\ViolationRuleEngineService;
|
||||
use App\Services\AttendanceTracking\AttendanceCaseQueryService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceCaseQueryServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_returns_not_found_when_student_missing(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_get_student_case_returns_404_when_student_not_found(): void
|
||||
{
|
||||
$student = Mockery::mock(Student::class);
|
||||
$student->shouldReceive('query->find')->with(99)->andReturn(null);
|
||||
|
||||
$attendanceData = Mockery::mock(AttendanceData::class);
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$rules = Mockery::mock(ViolationRuleEngineService::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new AttendanceCaseQueryService(
|
||||
$student,
|
||||
$attendanceData,
|
||||
$tracking,
|
||||
$rules,
|
||||
$parents
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\AttendanceTracking(),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class)
|
||||
);
|
||||
|
||||
$result = $service->getStudentCase(99);
|
||||
$result = $service->getStudentCase(999);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(404, $result['status']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-50
@@ -2,72 +2,62 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Services\AttendanceCommunicationSupportService;
|
||||
use App\Services\AttendanceEmailComposerService;
|
||||
use App\Services\AttendanceParentLookupService;
|
||||
use App\Services\AttendanceTracking\AttendanceCommunicationSupportService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceCommunicationSupportServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function makeService(
|
||||
?Student $student = null,
|
||||
?AttendanceData $attendanceData = null,
|
||||
?Configuration $config = null,
|
||||
?AttendanceParentLookupService $parents = null,
|
||||
?AttendanceEmailComposerService $composer = null
|
||||
): AttendanceCommunicationSupportService {
|
||||
$student ??= Mockery::mock(Student::class);
|
||||
$attendanceData ??= Mockery::mock(AttendanceData::class);
|
||||
$config ??= Mockery::mock(Configuration::class);
|
||||
$parents ??= Mockery::mock(AttendanceParentLookupService::class);
|
||||
$composer ??= Mockery::mock(AttendanceEmailComposerService::class);
|
||||
|
||||
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
|
||||
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
|
||||
|
||||
return new AttendanceCommunicationSupportService(
|
||||
$student,
|
||||
$attendanceData,
|
||||
$config,
|
||||
$parents,
|
||||
$composer
|
||||
);
|
||||
}
|
||||
|
||||
public function test_compose_returns_422_when_student_id_missing(): void
|
||||
public function test_compose_requires_student_id(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$result = $service->compose(0);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
|
||||
public function test_parents_info_delegates_to_parent_lookup_service(): void
|
||||
public function test_get_violation_dates_for_student(): void
|
||||
{
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
$parents->shouldReceive('parentsInfo')
|
||||
->once()
|
||||
->with(20, '2025-2026')
|
||||
->andReturn(['success' => true, 'data' => ['primary' => [], 'secondary' => []]]);
|
||||
DB::table('configuration')->insert([
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
]);
|
||||
|
||||
$service = $this->makeService(
|
||||
parents: $parents
|
||||
);
|
||||
DB::table('attendance_data')->insert([
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'student_id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'date' => '2025-01-01',
|
||||
'status' => 'absent',
|
||||
'is_reported' => 'no',
|
||||
'is_notified' => 'no',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$result = $service->parentsInfo(20);
|
||||
$service = $this->makeService();
|
||||
$dates = $service->getViolationDatesForStudent(1, 'ABS_1');
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(['2025-01-01'], $dates);
|
||||
}
|
||||
}
|
||||
|
||||
private function makeService(): AttendanceCommunicationSupportService
|
||||
{
|
||||
$parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class);
|
||||
$emailComposer = Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class);
|
||||
|
||||
return new AttendanceCommunicationSupportService(
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\Configuration(),
|
||||
$parentLookup,
|
||||
$emailComposer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,124 +2,37 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceEmailTemplate;
|
||||
use App\Services\AttendanceEmailComposerService;
|
||||
use Mockery;
|
||||
use App\Services\AttendanceTracking\AttendanceEmailComposerService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceEmailComposerServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_render_template_replaces_tokens(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_build_template_context_returns_expected_placeholders(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$context = $service->buildTemplateContext(
|
||||
[
|
||||
'name' => 'John Doe',
|
||||
'last_date' => '2026-03-01',
|
||||
],
|
||||
[
|
||||
'parent_name' => 'Jane Doe',
|
||||
'phone' => '555-1212',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame('Jane Doe', $context['{{parent_name}}']);
|
||||
$this->assertSame('John Doe', $context['{{student_name}}']);
|
||||
$this->assertSame('2026-03-01', $context['{{incident_date}}']);
|
||||
$this->assertSame('555-1212', $context['{{voicemail_phone}}']);
|
||||
}
|
||||
|
||||
public function test_render_template_uses_model_get_template(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$model->shouldReceive('getTemplate')
|
||||
->once()
|
||||
->with('ABS_1', 'default')
|
||||
->andReturn([
|
||||
'subject' => 'Notice for {{student_name}}',
|
||||
'body_html' => '<p>Date: {{incident_date}}</p>',
|
||||
]);
|
||||
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$rendered = $service->renderTemplate('ABS_1', 'default', [
|
||||
'{{student_name}}' => 'John Doe',
|
||||
'{{incident_date}}' => '2026-03-01',
|
||||
DB::table('email_templates')->insert([
|
||||
'code' => 'ABS_1',
|
||||
'variant' => 'default',
|
||||
'subject' => 'Notice for {{student_name}}',
|
||||
'body_html' => '<p>{{parent_name}}</p>',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
['Notice for John Doe', '<p>Date: 2026-03-01</p>'],
|
||||
$rendered
|
||||
);
|
||||
$service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate());
|
||||
$context = ['{{student_name}}' => 'Student A', '{{parent_name}}' => 'Parent A'];
|
||||
$rendered = $service->renderTemplate('ABS_1', 'default', $context);
|
||||
|
||||
$this->assertSame('Notice for Student A', $rendered[0]);
|
||||
$this->assertSame('<p>Parent A</p>', $rendered[1]);
|
||||
}
|
||||
|
||||
public function test_pick_variant_returns_override_when_present(): void
|
||||
public function test_pick_variant_fallback(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$this->assertSame('custom_variant', $service->pickVariant('ABS_2', 'custom_variant'));
|
||||
}
|
||||
|
||||
public function test_pick_variant_returns_no_answer_for_abs_2(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate());
|
||||
$this->assertSame('no_answer', $service->pickVariant('ABS_2'));
|
||||
}
|
||||
|
||||
public function test_pick_variant_returns_default_for_other_codes(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$this->assertSame('default', $service->pickVariant('ABS_1'));
|
||||
}
|
||||
|
||||
public function test_normalize_body_to_html_keeps_html_when_tags_present(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$body = '<p>Hello</p>';
|
||||
$this->assertSame($body, $service->normalizeBodyToHtml($body));
|
||||
}
|
||||
|
||||
public function test_normalize_body_to_html_converts_plain_text_to_br_html(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
$result = $service->normalizeBodyToHtml("Hello\nWorld");
|
||||
|
||||
$this->assertStringContainsString('Hello', $result);
|
||||
$this->assertStringContainsString('<br', $result);
|
||||
$this->assertStringContainsString('World', $result);
|
||||
}
|
||||
|
||||
public function test_build_fallback_auto_email_returns_subject_and_body(): void
|
||||
{
|
||||
$model = Mockery::mock(AttendanceEmailTemplate::class);
|
||||
$service = new AttendanceEmailComposerService($model);
|
||||
|
||||
[$subject, $body] = $service->buildFallbackAutoEmail('ABS_1', [
|
||||
'name' => 'John Doe',
|
||||
'violation' => '1 Unreported Absence',
|
||||
'last_date' => '2026-03-01',
|
||||
], 'Jane Doe');
|
||||
|
||||
$this->assertSame('Attendance Notice: Unreported Absence', $subject);
|
||||
$this->assertStringContainsString('John Doe', $body);
|
||||
$this->assertStringContainsString('2026-03-01', $body);
|
||||
$this->assertStringContainsString('Jane Doe', $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,45 +2,32 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\ParentNotification;
|
||||
use App\Services\AttendanceNotificationLogService;
|
||||
use Mockery;
|
||||
use App\Services\AttendanceTracking\AttendanceNotificationLogService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceNotificationLogServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_log_notification_creates_and_updates(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
$service = new AttendanceNotificationLogService(new \App\Models\ParentNotification());
|
||||
|
||||
$service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Subject', 'sent');
|
||||
$this->assertDatabaseHas('parent_notifications', [
|
||||
'student_id' => 1,
|
||||
'code' => 'ABS_1',
|
||||
'incident_date' => '2025-01-01',
|
||||
'channel' => 'email',
|
||||
'to_address' => 'parent@example.com',
|
||||
'status' => 'sent',
|
||||
]);
|
||||
|
||||
$service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Updated', 'failed');
|
||||
$row = DB::table('parent_notifications')->where('student_id', 1)->first();
|
||||
$this->assertSame('Updated', $row->subject);
|
||||
$this->assertSame('failed', $row->status);
|
||||
}
|
||||
|
||||
public function test_notification_already_sent_returns_true_when_model_has_sent_returns_true(): void
|
||||
{
|
||||
$model = Mockery::mock(ParentNotification::class);
|
||||
$model->shouldReceive('hasSent')
|
||||
->once()
|
||||
->with(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
|
||||
->andReturn(true);
|
||||
|
||||
$service = new AttendanceNotificationLogService($model);
|
||||
|
||||
$this->assertTrue(
|
||||
$service->notificationAlreadySent(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_notification_already_sent_returns_false_when_model_has_sent_returns_false(): void
|
||||
{
|
||||
$model = Mockery::mock(ParentNotification::class);
|
||||
$model->shouldReceive('hasSent')
|
||||
->once()
|
||||
->andReturn(false);
|
||||
|
||||
$service = new AttendanceNotificationLogService($model);
|
||||
|
||||
$this->assertFalse(
|
||||
$service->notificationAlreadySent(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
-98
@@ -2,114 +2,50 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceTracking;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Services\AttendanceEmailComposerService;
|
||||
use App\Services\AttendanceMailerService;
|
||||
use App\Services\AttendanceNotificationLogService;
|
||||
use App\Services\AttendanceNotificationWorkflowService;
|
||||
use App\Services\AttendanceParentLookupService;
|
||||
use App\Services\ViolationRuleEngineService;
|
||||
use App\Services\AttendanceTracking\AttendanceNotificationWorkflowService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceNotificationWorkflowServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_record_requires_parent_email(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function makeService(
|
||||
?Student $student = null,
|
||||
?StudentClass $studentClass = null,
|
||||
?AttendanceData $attendanceData = null,
|
||||
?AttendanceTracking $tracking = null,
|
||||
?Configuration $config = null,
|
||||
?AttendanceMailerService $mailer = null,
|
||||
?ViolationRuleEngineService $rules = null,
|
||||
?AttendanceParentLookupService $parents = null,
|
||||
?AttendanceEmailComposerService $composer = null,
|
||||
?AttendanceNotificationLogService $log = null
|
||||
): AttendanceNotificationWorkflowService {
|
||||
$student ??= Mockery::mock(Student::class);
|
||||
$studentClass ??= Mockery::mock(StudentClass::class);
|
||||
$attendanceData ??= Mockery::mock(AttendanceData::class);
|
||||
$tracking ??= Mockery::mock(AttendanceTracking::class);
|
||||
$config ??= Mockery::mock(Configuration::class);
|
||||
$mailer ??= Mockery::mock(AttendanceMailerService::class);
|
||||
$rules ??= Mockery::mock(ViolationRuleEngineService::class);
|
||||
$parents ??= Mockery::mock(AttendanceParentLookupService::class);
|
||||
$composer ??= Mockery::mock(AttendanceEmailComposerService::class);
|
||||
$log ??= Mockery::mock(AttendanceNotificationLogService::class);
|
||||
|
||||
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
|
||||
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
|
||||
|
||||
return new AttendanceNotificationWorkflowService(
|
||||
$student,
|
||||
$studentClass,
|
||||
$attendanceData,
|
||||
$tracking,
|
||||
$config,
|
||||
$mailer,
|
||||
$rules,
|
||||
$parents,
|
||||
$composer,
|
||||
$log
|
||||
);
|
||||
}
|
||||
|
||||
public function test_send_auto_emails_returns_empty_summary_when_no_auto_email_violations(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$result = $service->sendAutoEmails([
|
||||
['action' => 'team_notify'],
|
||||
DB::table('students')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame(0, $result['sent']);
|
||||
$this->assertSame(0, $result['skipped']);
|
||||
$this->assertSame(0, $result['errors']);
|
||||
}
|
||||
|
||||
public function test_send_email_manual_returns_failure_when_all_sends_fail(): void
|
||||
{
|
||||
$mailer = Mockery::mock(AttendanceMailerService::class);
|
||||
$mailer->shouldReceive('send')->andReturn(false);
|
||||
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
$parents->shouldReceive('getSecondaryParentForStudent')->andReturn(null);
|
||||
|
||||
$composer = Mockery::mock(AttendanceEmailComposerService::class);
|
||||
$composer->shouldReceive('normalizeBodyToHtml')->andReturn('<p>Hello</p>');
|
||||
$composer->shouldReceive('renderWithEmailLayout')->andReturn('<html>Hello</html>');
|
||||
|
||||
$log = Mockery::mock(AttendanceNotificationLogService::class);
|
||||
$log->shouldReceive('logNotification')->atLeast()->once();
|
||||
|
||||
$service = $this->makeService(
|
||||
mailer: $mailer,
|
||||
parents: $parents,
|
||||
composer: $composer,
|
||||
log: $log
|
||||
$service = new AttendanceNotificationWorkflowService(
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\StudentClass(),
|
||||
new \App\Models\AttendanceData(),
|
||||
new \App\Models\AttendanceTracking(),
|
||||
new \App\Models\Configuration(),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\AttendanceMailerService::class),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class),
|
||||
Mockery::mock(\App\Services\AttendanceTracking\AttendanceNotificationLogService::class)
|
||||
);
|
||||
|
||||
$result = $service->sendEmailManual([
|
||||
'student_id' => 10,
|
||||
'to' => 'parent@example.com',
|
||||
'subject' => 'Test',
|
||||
'body_html' => 'Hello',
|
||||
'code' => 'ABS_1',
|
||||
'incident_date' => '2026-03-01',
|
||||
], ['2026-03-01']);
|
||||
$result = $service->record([
|
||||
'student_id' => 1,
|
||||
'date' => '2025-01-01',
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertSame(500, $result['status']);
|
||||
$this->assertSame(422, $result['status']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Services\AttendanceParentLookupService;
|
||||
use App\Services\AttendanceTracking\AttendanceParentLookupService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
@@ -11,86 +11,78 @@ class AttendanceParentLookupServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_primary_parent_for_student_returns_parent_info(): void
|
||||
public function test_get_primary_parent_for_student(): void
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'id' => 100,
|
||||
'firstname' => 'Jane',
|
||||
'lastname' => 'Doe',
|
||||
'email' => 'jane@example.com',
|
||||
'cellphone' => '555-1111',
|
||||
'id' => 1,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'One',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 200,
|
||||
'parent_id' => 100,
|
||||
'id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new AttendanceParentLookupService();
|
||||
$parent = $service->getPrimaryParentForStudent(1);
|
||||
|
||||
$result = $service->getPrimaryParentForStudent(200);
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertSame(100, $result['user_id']);
|
||||
$this->assertSame('jane@example.com', $result['email']);
|
||||
$this->assertSame('Jane Doe', $result['parent_name']);
|
||||
$this->assertSame('parent@example.com', $parent['email']);
|
||||
}
|
||||
|
||||
public function test_get_secondary_parent_for_student_returns_secondary_user_when_present(): void
|
||||
public function test_get_secondary_parent_falls_back_to_parent_row(): void
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
['id' => 100, 'firstname' => 'Jane', 'lastname' => 'Doe', 'email' => 'jane@example.com', 'cellphone' => '555-1111'],
|
||||
['id' => 101, 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@example.com', 'cellphone' => '555-2222'],
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 200,
|
||||
'parent_id' => 100,
|
||||
'id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 11,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('parents')->insert([
|
||||
'firstparent_id' => 100,
|
||||
'secondparent_id' => 101,
|
||||
'secondparent_firstname' => 'Second',
|
||||
'secondparent_lastname' => 'Parent',
|
||||
'secondparent_gender' => 'Female',
|
||||
'secondparent_email' => 'second@example.com',
|
||||
'secondparent_phone' => '5551112222',
|
||||
'firstparent_id' => 11,
|
||||
'secondparent_id' => 0,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$service = new AttendanceParentLookupService();
|
||||
$parent = $service->getSecondaryParentForStudent(1, '2025-2026');
|
||||
|
||||
$result = $service->getSecondaryParentForStudent(200, '2025-2026');
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertSame(101, $result['user_id']);
|
||||
$this->assertSame('john@example.com', $result['email']);
|
||||
$this->assertSame('second@example.com', $parent['email']);
|
||||
}
|
||||
|
||||
public function test_parents_info_returns_primary_and_secondary_data(): void
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
['id' => 100, 'firstname' => 'Jane', 'lastname' => 'Doe', 'email' => 'jane@example.com', 'cellphone' => '555-1111'],
|
||||
['id' => 101, 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@example.com', 'cellphone' => '555-2222'],
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'id' => 200,
|
||||
'parent_id' => 100,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('parents')->insert([
|
||||
'firstparent_id' => 100,
|
||||
'secondparent_id' => 101,
|
||||
'school_year' => '2025-2026',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$service = new AttendanceParentLookupService();
|
||||
|
||||
$result = $service->parentsInfo(200, '2025-2026');
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertSame('jane@example.com', $result['data']['primary']['email']);
|
||||
$this->assertSame('john@example.com', $result['data']['secondary']['email']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,49 +2,39 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Services\AttendancePendingViolationService;
|
||||
use App\Services\AttendanceViolationStudentResolverService;
|
||||
use App\Services\ViolationRuleEngineService;
|
||||
use App\Services\AttendanceTracking\AttendancePendingViolationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendancePendingViolationServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_returns_error_when_no_students(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
$resolver = Mockery::mock(\App\Services\AttendanceTracking\AttendanceViolationStudentResolverService::class);
|
||||
$resolver->shouldReceive('resolveForSchoolYear')->andReturn([
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'student_ids' => [],
|
||||
'student_codes' => [],
|
||||
'students' => [],
|
||||
'student_code_to_id' => [],
|
||||
'existing_ids' => [],
|
||||
'debug' => [],
|
||||
]);
|
||||
|
||||
public function test_get_pending_violations_returns_error_when_no_student_identifiers_found(): void
|
||||
{
|
||||
$attendanceData = Mockery::mock(AttendanceData::class);
|
||||
$rules = Mockery::mock(ViolationRuleEngineService::class);
|
||||
$resolver = Mockery::mock(AttendanceViolationStudentResolverService::class);
|
||||
$engine = Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class);
|
||||
|
||||
$resolver->shouldReceive('resolveForSchoolYear')
|
||||
->once()
|
||||
->with('2025-2026', null)
|
||||
->andReturn([
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => null,
|
||||
'student_ids' => [],
|
||||
'student_codes' => [],
|
||||
'students' => [],
|
||||
'student_code_to_id' => [],
|
||||
'existing_ids' => [],
|
||||
'debug' => [
|
||||
'class_students' => 0,
|
||||
'student_ids' => 0,
|
||||
],
|
||||
]);
|
||||
$service = new AttendancePendingViolationService(
|
||||
new \App\Models\AttendanceData(),
|
||||
$engine,
|
||||
$resolver
|
||||
);
|
||||
|
||||
$service = new AttendancePendingViolationService($attendanceData, $rules, $resolver);
|
||||
$result = $service->getPendingViolations('2025-2026', null, null);
|
||||
|
||||
$result = $service->getPendingViolations('2025-2026');
|
||||
|
||||
$this->assertSame([], $result['students']);
|
||||
$this->assertSame('No student identifiers found for the current school year.', $result['error']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,134 +2,17 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceTracking;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Services\AttendanceCaseQueryService;
|
||||
use App\Services\AttendanceCommunicationSupportService;
|
||||
use App\Services\AttendanceEmailComposerService;
|
||||
use App\Services\AttendanceMailerService;
|
||||
use App\Services\AttendanceNotificationLogService;
|
||||
use App\Services\AttendanceNotificationWorkflowService;
|
||||
use App\Services\AttendanceParentLookupService;
|
||||
use App\Services\AttendancePendingViolationService;
|
||||
use App\Services\AttendanceTrackingService;
|
||||
use App\Services\AttendanceViolationStudentResolverService;
|
||||
use App\Services\ViolationRuleEngineService;
|
||||
use Mockery;
|
||||
use App\Services\AttendanceTracking\AttendanceTrackingService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceTrackingServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_service_instantiates(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
$service = new AttendanceTrackingService();
|
||||
$this->assertInstanceOf(AttendanceTrackingService::class, $service);
|
||||
}
|
||||
|
||||
protected function makeService(
|
||||
?AttendancePendingViolationService $pending = null,
|
||||
?AttendanceCaseQueryService $caseQuery = null,
|
||||
?AttendanceNotificationWorkflowService $workflow = null,
|
||||
?AttendanceCommunicationSupportService $support = null
|
||||
): AttendanceTrackingService {
|
||||
$student = Mockery::mock(Student::class);
|
||||
$studentClass = Mockery::mock(StudentClass::class);
|
||||
$attendanceData = Mockery::mock(AttendanceData::class);
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$config = Mockery::mock(Configuration::class);
|
||||
$mailer = Mockery::mock(AttendanceMailerService::class);
|
||||
$rules = Mockery::mock(ViolationRuleEngineService::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
$composer = Mockery::mock(AttendanceEmailComposerService::class);
|
||||
$log = Mockery::mock(AttendanceNotificationLogService::class);
|
||||
$resolver = Mockery::mock(AttendanceViolationStudentResolverService::class);
|
||||
|
||||
$pending ??= Mockery::mock(AttendancePendingViolationService::class);
|
||||
$caseQuery ??= Mockery::mock(AttendanceCaseQueryService::class);
|
||||
$workflow ??= Mockery::mock(AttendanceNotificationWorkflowService::class);
|
||||
$support ??= Mockery::mock(AttendanceCommunicationSupportService::class);
|
||||
|
||||
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
|
||||
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
|
||||
|
||||
return new AttendanceTrackingService(
|
||||
$student,
|
||||
$studentClass,
|
||||
$attendanceData,
|
||||
$tracking,
|
||||
$config,
|
||||
$mailer,
|
||||
$rules,
|
||||
$parents,
|
||||
$composer,
|
||||
$log,
|
||||
$resolver,
|
||||
$caseQuery,
|
||||
$workflow,
|
||||
$support,
|
||||
$pending
|
||||
);
|
||||
}
|
||||
|
||||
public function test_get_pending_violations_delegates_to_pending_service(): void
|
||||
{
|
||||
$pending = Mockery::mock(AttendancePendingViolationService::class);
|
||||
$pending->shouldReceive('getPendingViolations')
|
||||
->once()
|
||||
->with('2025-2026', null, null)
|
||||
->andReturn(['students' => []]);
|
||||
|
||||
$service = $this->makeService(pending: $pending);
|
||||
|
||||
$result = $service->getPendingViolations();
|
||||
|
||||
$this->assertSame(['students' => []], $result);
|
||||
}
|
||||
|
||||
public function test_get_student_case_delegates_to_case_query_service(): void
|
||||
{
|
||||
$caseQuery = Mockery::mock(AttendanceCaseQueryService::class);
|
||||
$caseQuery->shouldReceive('getStudentCase')
|
||||
->once()
|
||||
->andReturn(['success' => true]);
|
||||
|
||||
$service = $this->makeService(caseQuery: $caseQuery);
|
||||
|
||||
$result = $service->getStudentCase(1);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
}
|
||||
|
||||
public function test_record_delegates_to_workflow_service(): void
|
||||
{
|
||||
$workflow = Mockery::mock(AttendanceNotificationWorkflowService::class);
|
||||
$workflow->shouldReceive('record')
|
||||
->once()
|
||||
->with(['student_id' => 10])
|
||||
->andReturn(['success' => true]);
|
||||
|
||||
$service = $this->makeService(workflow: $workflow);
|
||||
|
||||
$result = $service->record(['student_id' => 10]);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
}
|
||||
|
||||
public function test_compose_delegates_to_support_service(): void
|
||||
{
|
||||
$support = Mockery::mock(AttendanceCommunicationSupportService::class);
|
||||
$support->shouldReceive('compose')
|
||||
->once()
|
||||
->with(10, 'ABS_1', 'default', null)
|
||||
->andReturn(['success' => true]);
|
||||
|
||||
$service = $this->makeService(support: $support);
|
||||
|
||||
$result = $service->compose(10);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-54
@@ -2,67 +2,46 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Services\AttendanceViolationStudentResolverService;
|
||||
use Mockery;
|
||||
use App\Services\AttendanceTracking\AttendanceViolationStudentResolverService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceViolationStudentResolverServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_resolve_for_school_year_returns_students(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
DB::table('students')->insert([
|
||||
'id' => 1,
|
||||
'school_id' => 'S1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
public function test_ensure_attendance_student_exists_keeps_numeric_student(): void
|
||||
{
|
||||
$student = Mockery::mock(Student::class);
|
||||
$studentClass = Mockery::mock(StudentClass::class);
|
||||
$attendanceData = Mockery::mock(AttendanceData::class);
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 10,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new AttendanceViolationStudentResolverService($student, $studentClass, $attendanceData);
|
||||
|
||||
$students = [];
|
||||
$studentCodeToId = [];
|
||||
$existingIds = [];
|
||||
|
||||
$sid = $service->ensureAttendanceStudentExists(
|
||||
$students,
|
||||
$studentCodeToId,
|
||||
$existingIds,
|
||||
123
|
||||
$service = new AttendanceViolationStudentResolverService(
|
||||
new \App\Models\Student(),
|
||||
new \App\Models\StudentClass(),
|
||||
new \App\Models\AttendanceData()
|
||||
);
|
||||
|
||||
$this->assertSame(123, $sid);
|
||||
$this->assertCount(1, $students);
|
||||
$this->assertSame(123, $students[0]['id']);
|
||||
$result = $service->resolveForSchoolYear('2025-2026', 'Fall');
|
||||
|
||||
$this->assertContains(1, $result['student_ids']);
|
||||
$this->assertNotEmpty($result['students']);
|
||||
}
|
||||
|
||||
public function test_ensure_attendance_student_exists_creates_placeholder_for_code(): void
|
||||
{
|
||||
$student = Mockery::mock(Student::class);
|
||||
$studentClass = Mockery::mock(StudentClass::class);
|
||||
$attendanceData = Mockery::mock(AttendanceData::class);
|
||||
|
||||
$service = new AttendanceViolationStudentResolverService($student, $studentClass, $attendanceData);
|
||||
|
||||
$students = [];
|
||||
$studentCodeToId = [];
|
||||
$existingIds = [];
|
||||
|
||||
$sid = $service->ensureAttendanceStudentExists(
|
||||
$students,
|
||||
$studentCodeToId,
|
||||
$existingIds,
|
||||
'SCH-001'
|
||||
);
|
||||
|
||||
$this->assertNotNull($sid);
|
||||
$this->assertCount(1, $students);
|
||||
$this->assertSame('SCH-001', $students[0]['school_id']);
|
||||
$this->assertArrayHasKey('SCH-001', $studentCodeToId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,111 +2,37 @@
|
||||
|
||||
namespace Tests\Unit\Services\AttendanceTracking;
|
||||
|
||||
use App\Models\AttendanceTracking;
|
||||
use App\Services\AttendanceParentLookupService;
|
||||
use App\Services\ViolationRuleEngineService;
|
||||
use App\Services\AttendanceTracking\ViolationRuleEngineService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ViolationRuleEngineServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_school_year_helpers(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame(['2025-08-01', '2026-07-31'], $service->deriveSchoolYearBounds('2025-2026'));
|
||||
$this->assertSame('2025-2026', $service->schoolYearForDate('2025-09-01'));
|
||||
}
|
||||
|
||||
public function test_choose_higher_severity_returns_higher_severity_rule(): void
|
||||
public function test_window_weeks_for_violation_code(): void
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
|
||||
$result = $service->chooseHigherSeverity(
|
||||
['severity' => 2, 'action' => 'team_notify'],
|
||||
['severity' => 4, 'action' => 'auto_email']
|
||||
);
|
||||
|
||||
$this->assertSame(4, $result['severity']);
|
||||
}
|
||||
|
||||
public function test_choose_higher_severity_breaks_ties_by_action_priority(): void
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
|
||||
$result = $service->chooseHigherSeverity(
|
||||
['severity' => 3, 'action' => 'team_notify'],
|
||||
['severity' => 3, 'action' => 'auto_email']
|
||||
);
|
||||
|
||||
$this->assertSame('team_notify', $result['action']);
|
||||
}
|
||||
|
||||
public function test_window_weeks_for_violation_code_returns_expected_values(): void
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame(3, $service->windowWeeksForViolationCode('ABS_2'));
|
||||
$this->assertSame(4, $service->windowWeeksForViolationCode('ABS_3'));
|
||||
$this->assertSame(4, $service->windowWeeksForViolationCode('MIX'));
|
||||
$this->assertSame(5, $service->windowWeeksForViolationCode('ABS_4'));
|
||||
$this->assertSame(4, $service->windowWeeksForViolationCode('LATE_4'));
|
||||
}
|
||||
|
||||
public function test_has_n_consecutive_items_detects_weekly_sequence(): void
|
||||
private function makeService(): ViolationRuleEngineService
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
$tracking = new \App\Models\AttendanceTracking();
|
||||
$parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
|
||||
$result = $service->hasNConsecutiveItems(
|
||||
['2026-03-15', '2026-03-08', '2026-03-01'],
|
||||
3,
|
||||
7
|
||||
);
|
||||
|
||||
$this->assertTrue($result);
|
||||
return new ViolationRuleEngineService($tracking, $parentLookup);
|
||||
}
|
||||
|
||||
public function test_has_n_in_w_active_weeks_detects_window_match(): void
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
|
||||
$this->assertTrue($service->hasNInWActiveWeeks([0, 1], 2, 3));
|
||||
$this->assertFalse($service->hasNInWActiveWeeks([0], 2, 3));
|
||||
}
|
||||
|
||||
public function test_two_lates_one_abs_in_w_weeks_detects_mix_rule(): void
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
|
||||
$this->assertTrue($service->twoLatesOneAbsInWWeeks([1, 2], [3], 4));
|
||||
$this->assertFalse($service->twoLatesOneAbsInWWeeks([1], [3], 4));
|
||||
}
|
||||
|
||||
public function test_derive_school_year_bounds_returns_expected_dates(): void
|
||||
{
|
||||
$tracking = Mockery::mock(AttendanceTracking::class);
|
||||
$parents = Mockery::mock(AttendanceParentLookupService::class);
|
||||
|
||||
$service = new ViolationRuleEngineService($tracking, $parents);
|
||||
|
||||
[$start, $end] = $service->deriveSchoolYearBounds('2025-2026');
|
||||
|
||||
$this->assertSame('2025-08-01', $start);
|
||||
$this->assertSame('2026-07-31', $end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Auth;
|
||||
|
||||
use App\Services\Auth\RegistrationCaptchaService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationCaptchaServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_generate_sets_session_value(): void
|
||||
{
|
||||
$service = new RegistrationCaptchaService();
|
||||
$value = $service->generate(6);
|
||||
|
||||
$this->assertNotEmpty($value);
|
||||
$this->assertSame($value, session()->get('captcha_answer'));
|
||||
}
|
||||
|
||||
public function test_verify_and_clear(): void
|
||||
{
|
||||
$service = new RegistrationCaptchaService();
|
||||
session()->put('captcha_answer', 'ABC123');
|
||||
|
||||
$this->assertTrue($service->verify('ABC123'));
|
||||
$this->assertFalse($service->verify('WRONG'));
|
||||
|
||||
$service->clear();
|
||||
$this->assertNull(session()->get('captcha_answer'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Auth;
|
||||
|
||||
use App\Services\Auth\RegistrationFormatterService;
|
||||
use App\Services\PhoneFormatterService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationFormatterServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_format_normalizes_parent_fields(): void
|
||||
{
|
||||
$formatter = new RegistrationFormatterService(new PhoneFormatterService());
|
||||
|
||||
$result = $formatter->format([
|
||||
'firstname' => 'john',
|
||||
'lastname' => 'doe',
|
||||
'email' => 'USER@EXAMPLE.COM',
|
||||
'gender' => 'Male',
|
||||
'city' => 'new haven',
|
||||
'cellphone' => '(203) 555-1234',
|
||||
'address_street' => '123 main st',
|
||||
'apt' => 'a1',
|
||||
'state' => 'ct',
|
||||
'zip' => '06510',
|
||||
'accept_school_policy' => 1,
|
||||
'no_second_parent_info' => 1,
|
||||
]);
|
||||
|
||||
$this->assertSame('John', $result['firstname']);
|
||||
$this->assertSame('Doe', $result['lastname']);
|
||||
$this->assertSame('user@example.com', $result['email']);
|
||||
$this->assertSame('New Haven', $result['city']);
|
||||
$this->assertSame('203-555-1234', $result['cellphone']);
|
||||
$this->assertSame('CT', $result['state']);
|
||||
$this->assertArrayNotHasKey('second_firstname', $result);
|
||||
}
|
||||
|
||||
public function test_format_includes_second_parent_when_present(): void
|
||||
{
|
||||
$formatter = new RegistrationFormatterService(new PhoneFormatterService());
|
||||
|
||||
$result = $formatter->format([
|
||||
'firstname' => 'john',
|
||||
'lastname' => 'doe',
|
||||
'email' => 'user@example.com',
|
||||
'gender' => 'Male',
|
||||
'city' => 'new haven',
|
||||
'cellphone' => '2035551234',
|
||||
'address_street' => '123 main st',
|
||||
'apt' => 'a1',
|
||||
'state' => 'ct',
|
||||
'zip' => '06510',
|
||||
'accept_school_policy' => 1,
|
||||
'second_firstname' => 'jane',
|
||||
'second_lastname' => 'doe',
|
||||
'second_gender' => 'Female',
|
||||
'second_email' => 'jane@example.com',
|
||||
'second_cellphone' => '2035559876',
|
||||
]);
|
||||
|
||||
$this->assertSame('Jane', $result['second_firstname']);
|
||||
$this->assertSame('Doe', $result['second_lastname']);
|
||||
$this->assertSame('jane@example.com', $result['second_email']);
|
||||
$this->assertSame('203-555-9876', $result['second_cellphone']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Auth;
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\RegistrationCaptchaService;
|
||||
use App\Services\Auth\RegistrationFormatterService;
|
||||
use App\Services\Auth\RegistrationService;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\PhoneFormatterService;
|
||||
use App\Services\SchoolIdService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_register_creates_parent_user(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
Role::query()->create([
|
||||
'id' => 1,
|
||||
'name' => 'parent',
|
||||
'slug' => 'parent',
|
||||
'description' => 'Parent',
|
||||
'priority' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
session()->put('captcha_answer', 'ABCD');
|
||||
|
||||
$emailService = Mockery::mock(EmailService::class);
|
||||
$emailService->shouldReceive('send')->once()->andReturn(true);
|
||||
|
||||
$service = new RegistrationService(
|
||||
$emailService,
|
||||
new SchoolIdService(),
|
||||
new RegistrationFormatterService(new PhoneFormatterService()),
|
||||
new RegistrationCaptchaService()
|
||||
);
|
||||
|
||||
$result = $service->register([
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'gender' => 'Male',
|
||||
'email' => 'parent@example.com',
|
||||
'confirm_email' => 'parent@example.com',
|
||||
'cellphone' => '5555555555',
|
||||
'address_street' => '123 Main',
|
||||
'apt' => 'A',
|
||||
'city' => 'City',
|
||||
'state' => 'CT',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'captcha' => 'ABCD',
|
||||
'is_parent' => 1,
|
||||
'no_second_parent_info' => 1,
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'parent@example.com',
|
||||
'status' => 'Inactive',
|
||||
]);
|
||||
$this->assertDatabaseHas('user_roles', [
|
||||
'user_id' => $result['user']->id,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_register_rejects_pending_activation(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
Role::query()->create([
|
||||
'id' => 1,
|
||||
'name' => 'parent',
|
||||
'slug' => 'parent',
|
||||
'description' => 'Parent',
|
||||
'priority' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
User::query()->create([
|
||||
'school_id' => '2500001',
|
||||
'firstname' => 'Existing',
|
||||
'lastname' => 'User',
|
||||
'gender' => 'Male',
|
||||
'cellphone' => '555-555-5555',
|
||||
'email' => 'existing@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'CT',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 0,
|
||||
'status' => 'Inactive',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => pbkdf2_hash('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'token' => 'token',
|
||||
]);
|
||||
|
||||
session()->put('captcha_answer', 'ABCD');
|
||||
|
||||
$emailService = Mockery::mock(EmailService::class);
|
||||
$emailService->shouldNotReceive('send');
|
||||
|
||||
$service = new RegistrationService(
|
||||
$emailService,
|
||||
new SchoolIdService(),
|
||||
new RegistrationFormatterService(new PhoneFormatterService()),
|
||||
new RegistrationCaptchaService()
|
||||
);
|
||||
|
||||
$result = $service->register([
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'gender' => 'Male',
|
||||
'email' => 'existing@example.com',
|
||||
'confirm_email' => 'existing@example.com',
|
||||
'cellphone' => '5555555555',
|
||||
'address_street' => '123 Main',
|
||||
'apt' => 'A',
|
||||
'city' => 'City',
|
||||
'state' => 'CT',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'captcha' => 'ABCD',
|
||||
'is_parent' => 1,
|
||||
'no_second_parent_info' => 1,
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('pending_activation', $result['code']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\BroadcastEmail;
|
||||
|
||||
use App\Services\BroadcastEmail\BroadcastEmailComposerService;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BroadcastEmailComposerServiceTest extends TestCase
|
||||
{
|
||||
public function test_sanitize_removes_script_and_events(): void
|
||||
{
|
||||
$service = new BroadcastEmailComposerService();
|
||||
$html = '<p onclick="alert(1)">Hi</p><script>alert(2)</script>';
|
||||
|
||||
$clean = $service->sanitizeHtml($html);
|
||||
|
||||
$this->assertStringNotContainsString('script', $clean);
|
||||
$this->assertStringNotContainsString('onclick', $clean);
|
||||
}
|
||||
|
||||
public function test_compose_replaces_name_when_personalized(): void
|
||||
{
|
||||
$service = new BroadcastEmailComposerService();
|
||||
$html = '<p>Hello {{name}}</p>';
|
||||
|
||||
$rendered = $service->compose(false, 'Subject', $html, 'Parent', '', '', '', true);
|
||||
|
||||
$this->assertSame('<p>Hello Parent</p>', $rendered);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\BroadcastEmail;
|
||||
|
||||
use App\Services\BroadcastEmail\BroadcastEmailRecipientService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BroadcastEmailRecipientServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_parents_with_emails_filters_missing_email(): void
|
||||
{
|
||||
$this->seedParentData();
|
||||
|
||||
$service = new BroadcastEmailRecipientService();
|
||||
$parents = $service->parentsWithEmails();
|
||||
|
||||
$this->assertCount(1, $parents);
|
||||
$this->assertSame('parent@example.com', $parents[0]['email']);
|
||||
}
|
||||
|
||||
public function test_recipients_by_ids_returns_names(): void
|
||||
{
|
||||
$this->seedParentData();
|
||||
|
||||
$service = new BroadcastEmailRecipientService();
|
||||
$recipients = $service->recipientsByIds([10]);
|
||||
|
||||
$this->assertCount(1, $recipients);
|
||||
$this->assertSame('Parent User', $recipients[0]['name']);
|
||||
}
|
||||
|
||||
private function seedParentData(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
'id' => 1,
|
||||
'name' => 'parent',
|
||||
'slug' => 'parent',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 11,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'No',
|
||||
'lastname' => 'Email',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => '',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 10, 'role_id' => 1],
|
||||
['user_id' => 11, 'role_id' => 1],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\BroadcastEmail;
|
||||
|
||||
use App\Services\BroadcastEmail\BroadcastEmailSenderOptionsService;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BroadcastEmailSenderOptionsServiceTest extends TestCase
|
||||
{
|
||||
public function test_list_options_from_env(): void
|
||||
{
|
||||
$old = getenv('MAIL_SENDERS') ?: '';
|
||||
putenv('MAIL_SENDERS={"general":{"name":"Office","email":"office@example.com"}}');
|
||||
|
||||
$service = new BroadcastEmailSenderOptionsService();
|
||||
$options = $service->listOptions();
|
||||
|
||||
$this->assertSame('general', $options[0]['key']);
|
||||
$this->assertSame('Office <office@example.com>', $options[0]['label']);
|
||||
|
||||
putenv('MAIL_SENDERS=' . $old);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPrep;
|
||||
|
||||
use App\Services\ClassPrep\ClassRosterService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassRosterServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_students_by_class(): void
|
||||
{
|
||||
$this->seedRosterData();
|
||||
|
||||
$service = new ClassRosterService();
|
||||
$students = $service->listStudentsByClass(101, '2025-2026');
|
||||
|
||||
$this->assertCount(1, $students);
|
||||
$this->assertSame('1-A', $students[0]['registration_grade']);
|
||||
}
|
||||
|
||||
private function seedRosterData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
'id' => 1,
|
||||
'class_name' => 'Class 1',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPrep;
|
||||
|
||||
use App\Services\ClassPrep\StickerCountService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StickerCountServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_all_returns_totals_excluding_youth(): void
|
||||
{
|
||||
$this->seedStickerData();
|
||||
|
||||
$service = new StickerCountService();
|
||||
$payload = $service->listAll('2025-2026', 'Fall');
|
||||
|
||||
$this->assertSame(2, $payload['totals']['students']);
|
||||
$this->assertSame(6, $payload['totals']['primary']);
|
||||
$this->assertSame(1, $payload['totals']['secondary']);
|
||||
}
|
||||
|
||||
public function test_list_for_class_limits_results(): void
|
||||
{
|
||||
$this->seedStickerData();
|
||||
|
||||
$service = new StickerCountService();
|
||||
$payload = $service->listForClass('2025-2026', 'Fall', 101);
|
||||
|
||||
$this->assertSame(1, $payload['totals']['students']);
|
||||
$this->assertSame(3, $payload['totals']['primary']);
|
||||
$this->assertSame(0, $payload['totals']['secondary']);
|
||||
}
|
||||
|
||||
private function seedStickerData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_name' => 'Class 1',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_name' => 'Class 2',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_name' => 'Class 3',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'class_section_name' => '5-B',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'class_section_name' => 'Youth-1',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
[
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-2',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Two',
|
||||
'age' => 10,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-3',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Three',
|
||||
'age' => 12,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationAdjustmentService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationAdjustmentServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_apply_adjustments_updates_counts(): void
|
||||
{
|
||||
DB::table('class_prep_adjustments')->insert([
|
||||
'class_section_id' => 101,
|
||||
'item_name' => 'Small Table',
|
||||
'adjustment' => 2,
|
||||
'school_year' => '2025-2026',
|
||||
'adjustable' => 1,
|
||||
]);
|
||||
|
||||
$service = new ClassPreparationAdjustmentService();
|
||||
[$items, $adjMap] = $service->applyAdjustments(['Small Table' => 1], '101', '2025-2026');
|
||||
|
||||
$this->assertSame(3, $items['Small Table']);
|
||||
$this->assertSame(2, $adjMap['Small Table']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationCalculatorService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationCalculatorServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_class_level_by_section(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '2-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new ClassPreparationCalculatorService();
|
||||
|
||||
$this->assertSame(2, $service->getClassLevelBySection('101'));
|
||||
$this->assertSame(1, $service->getClassLevelBySection('KG'));
|
||||
}
|
||||
|
||||
public function test_calculate_prep_items_for_lower_grades(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
]);
|
||||
|
||||
DB::table('inventory_categories')->insert([
|
||||
['name' => 'Small Table', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
|
||||
['name' => 'Small Chair', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
|
||||
['name' => 'Teacher Chair', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
|
||||
['name' => 'Grade Box', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'class_section_id' => 101,
|
||||
'teacher_id' => 1,
|
||||
'position' => 'main',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new ClassPreparationCalculatorService();
|
||||
$items = $service->calculatePrepItems(6, 1, '101');
|
||||
|
||||
$this->assertSame(2, $items['Small Table']);
|
||||
$this->assertSame(6, $items['Small Chair']);
|
||||
$this->assertSame(1, $items['Teacher Chair']);
|
||||
$this->assertSame(1, $items['Grade Box']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationContextService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationContextServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_has_roster_for_semester(): void
|
||||
{
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$service = new ClassPreparationContextService();
|
||||
|
||||
$this->assertTrue($service->hasRosterForSemester('2025-2026', 'Fall'));
|
||||
$this->assertFalse($service->hasRosterForSemester('2025-2026', 'Spring'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationInventoryService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationInventoryServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_build_availability_uses_max_value(): void
|
||||
{
|
||||
DB::table('inventory_categories')->insert([
|
||||
'id' => 1,
|
||||
'type' => 'classroom',
|
||||
'name' => 'Small Table',
|
||||
]);
|
||||
|
||||
DB::table('inventory_items')->insert([
|
||||
[
|
||||
'type' => 'classroom',
|
||||
'category_id' => 1,
|
||||
'name' => 'Small Table',
|
||||
'quantity' => 5,
|
||||
'good_qty' => 5,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
],
|
||||
[
|
||||
'type' => 'classroom',
|
||||
'category_id' => 1,
|
||||
'name' => 'Small Table',
|
||||
'quantity' => 3,
|
||||
'good_qty' => 3,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
],
|
||||
]);
|
||||
|
||||
$service = new ClassPreparationInventoryService();
|
||||
$map = $service->buildAvailability('2025-2026', 'Fall', true, ['Small Table']);
|
||||
|
||||
$this->assertSame(8, $map['Small Table']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationLogService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationLogServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_has_prep_changed_detects_diff(): void
|
||||
{
|
||||
$service = new ClassPreparationLogService();
|
||||
|
||||
$this->assertTrue($service->hasPrepChanged(['a' => 1], ['a' => 2]));
|
||||
$this->assertFalse($service->hasPrepChanged(['a' => 1], ['a' => 1]));
|
||||
}
|
||||
|
||||
public function test_create_log_and_get_latest(): void
|
||||
{
|
||||
$service = new ClassPreparationLogService();
|
||||
$created = $service->createLog('101', '1-A', '2025-2026', ['Small Table' => 2], '2025-09-01 00:00:00');
|
||||
|
||||
$this->assertTrue($created);
|
||||
$this->assertDatabaseHas('class_preparation_log', [
|
||||
'class_section_id' => 101,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$log = $service->getLatestLog('101', '2025-2026');
|
||||
$this->assertNotNull($log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationRosterService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationRosterServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_class_section_student_counts(): void
|
||||
{
|
||||
$this->seedRoster();
|
||||
|
||||
$service = new ClassPreparationRosterService();
|
||||
$rows = $service->getClassSectionStudentCounts('2025-2026', 'Fall', true);
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame(101, (int) $rows[0]->class_section_id);
|
||||
$this->assertSame(2, (int) $rows[0]->student_count);
|
||||
}
|
||||
|
||||
public function test_get_student_count_for_section(): void
|
||||
{
|
||||
$this->seedRoster();
|
||||
|
||||
$service = new ClassPreparationRosterService();
|
||||
$count = $service->getStudentCountForSection('2025-2026', 'Fall', true, '101');
|
||||
|
||||
$this->assertSame(2, $count);
|
||||
}
|
||||
|
||||
private function seedRoster(): void
|
||||
{
|
||||
DB::table('students')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'school_id' => 'S-2',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Two',
|
||||
'age' => 9,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassPreparation;
|
||||
|
||||
use App\Services\ClassPreparation\ClassPreparationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPreparationServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_prep_returns_sections_and_totals(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
|
||||
$service = new ClassPreparationService();
|
||||
$payload = $service->listPrep('2025-2026', 'Fall');
|
||||
|
||||
$this->assertSame('2025-2026', $payload['schoolYear']);
|
||||
$this->assertSame('Fall', $payload['semester']);
|
||||
$this->assertNotEmpty($payload['results']);
|
||||
$this->assertArrayHasKey('Small Table', $payload['totals']);
|
||||
}
|
||||
|
||||
public function test_mark_printed_creates_logs(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
|
||||
$service = new ClassPreparationService();
|
||||
$count = $service->markPrinted('2025-2026', 'Fall', [101]);
|
||||
|
||||
$this->assertSame(1, $count);
|
||||
$this->assertDatabaseHas('class_preparation_log', [
|
||||
'class_section_id' => 101,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedPrepData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('inventory_categories')->insert([
|
||||
'type' => 'classroom',
|
||||
'name' => 'Small Table',
|
||||
]);
|
||||
|
||||
DB::table('inventory_items')->insert([
|
||||
'type' => 'classroom',
|
||||
'category_id' => 1,
|
||||
'name' => 'Small Table',
|
||||
'quantity' => 10,
|
||||
'good_qty' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Communication;
|
||||
|
||||
use App\Services\Communication\CommunicationTemplateService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CommunicationTemplateServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_active_templates_maps_columns(): void
|
||||
{
|
||||
DB::table('email_templates')->insert([
|
||||
'id' => 1,
|
||||
'code' => 'welcome',
|
||||
'variant' => 'default',
|
||||
'subject' => 'Hello',
|
||||
'body_html' => 'Body',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$service = new CommunicationTemplateService();
|
||||
$templates = $service->listActiveTemplates();
|
||||
|
||||
$this->assertSame('welcome', $templates[0]['template_key']);
|
||||
$this->assertSame('Body', $templates[0]['body']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\CompetitionScores;
|
||||
|
||||
use App\Models\CompetitionScore;
|
||||
use App\Services\CompetitionScores\CompetitionScoresSaveService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CompetitionScoresSaveServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_filter_scores_rejects_non_integers(): void
|
||||
{
|
||||
$service = new CompetitionScoresSaveService();
|
||||
[$clean, $invalid] = $service->filterScores([
|
||||
1 => '10',
|
||||
2 => '4.5',
|
||||
]);
|
||||
|
||||
$this->assertSame([1 => 10], $clean);
|
||||
$this->assertSame([2], $invalid);
|
||||
}
|
||||
|
||||
public function test_save_scores_upserts(): void
|
||||
{
|
||||
DB::table('competition_scores')->insert([
|
||||
'competition_id' => 1,
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'score' => 2,
|
||||
]);
|
||||
|
||||
$service = new CompetitionScoresSaveService();
|
||||
$service->saveScores(1, 101, [1 => 7]);
|
||||
|
||||
$this->assertDatabaseHas('competition_scores', [
|
||||
'competition_id' => 1,
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'score' => 7,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Discounts;
|
||||
|
||||
use App\Services\Discounts\DiscountApplyService;
|
||||
use App\Services\Discounts\DiscountContextService;
|
||||
use App\Services\Discounts\DiscountInvoiceService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DiscountApplyServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_apply_voucher_returns_error_when_no_remaining_uses(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('discount_vouchers')->insert([
|
||||
'id' => 1,
|
||||
'code' => 'USED-OUT',
|
||||
'discount_type' => 'fixed',
|
||||
'discount_value' => 10,
|
||||
'max_uses' => 1,
|
||||
'times_used' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$service = new DiscountApplyService(new DiscountContextService(), new DiscountInvoiceService());
|
||||
$result = $service->applyVoucher(1, [10], true, 1);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Expenses;
|
||||
|
||||
use App\Services\Expenses\ExpenseReceiptService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExpenseReceiptServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_receipt_returns_filename_and_url(): void
|
||||
{
|
||||
Storage::fake();
|
||||
|
||||
$service = new ExpenseReceiptService();
|
||||
$file = UploadedFile::fake()->create('receipt.pdf', 10, 'application/pdf');
|
||||
$filename = $service->storeReceipt($file);
|
||||
|
||||
$this->assertNotEmpty($filename);
|
||||
$this->assertSame(url('receipts/' . $filename), $service->receiptUrl($filename));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Expenses;
|
||||
|
||||
use App\Services\Expenses\ExpenseStaffService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExpenseStaffServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_staff_users_excludes_parents(): void
|
||||
{
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'administrator', 'slug' => 'administrator', 'is_active' => 1],
|
||||
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
|
||||
]);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 10,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Staff',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'staff@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 11,
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'parent@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => 10, 'role_id' => 1],
|
||||
['user_id' => 11, 'role_id' => 2],
|
||||
]);
|
||||
|
||||
$service = new ExpenseStaffService();
|
||||
$staff = $service->listStaffUsers();
|
||||
|
||||
$this->assertCount(1, $staff);
|
||||
$this->assertSame(10, $staff[0]['id']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ExtraCharges;
|
||||
|
||||
use App\Services\ExtraCharges\ExtraChargesMetaService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExtraChargesMetaServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_get_school_years_uses_fallback(): void
|
||||
{
|
||||
$service = new ExtraChargesMetaService();
|
||||
$years = $service->getSchoolYears('2025-2026');
|
||||
|
||||
$this->assertSame(['2025-2026', '2024-2025', '2023-2024', '2022-2023'], $years);
|
||||
}
|
||||
|
||||
public function test_get_school_years_merges_sources(): void
|
||||
{
|
||||
DB::table('additional_charges')->insert([
|
||||
[
|
||||
'parent_id' => 1,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'charge_type' => 'add',
|
||||
'title' => 'Test',
|
||||
'amount' => 1,
|
||||
],
|
||||
[
|
||||
'parent_id' => 1,
|
||||
'school_year' => '2024-2025',
|
||||
'semester' => 'Fall',
|
||||
'charge_type' => 'add',
|
||||
'title' => 'Test',
|
||||
'amount' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('invoices')->insert([
|
||||
[
|
||||
'parent_id' => 1,
|
||||
'invoice_number' => 'INV-1',
|
||||
'total_amount' => 10,
|
||||
'balance' => 10,
|
||||
'paid_amount' => 0,
|
||||
'issue_date' => '2025-09-01',
|
||||
'status' => 'Unpaid',
|
||||
'school_year' => '2023-2024',
|
||||
],
|
||||
]);
|
||||
|
||||
$service = new ExtraChargesMetaService();
|
||||
$years = $service->getSchoolYears();
|
||||
|
||||
$this->assertSame(['2025-2026', '2024-2025', '2023-2024'], $years);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Files;
|
||||
|
||||
use App\Services\Files\ExamDraftDownloadNameService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExamDraftDownloadNameServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_build_returns_slugified_name(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => '2B',
|
||||
'class_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('exam_drafts')->insert([
|
||||
'id' => 1,
|
||||
'teacher_id' => 1,
|
||||
'class_section_id' => 1,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'exam_type' => 'Final Exam',
|
||||
'draft_title' => 'Draft',
|
||||
'teacher_file' => 'draft2.pdf',
|
||||
'version' => 3,
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$service = new ExamDraftDownloadNameService();
|
||||
$name = $service->build('draft2.pdf', 'drafts');
|
||||
|
||||
$this->assertSame('2b_final_exam_v3', $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Files;
|
||||
|
||||
use App\Services\Files\FileServeService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FileServeServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_meta_returns_file_metadata(): void
|
||||
{
|
||||
$dir = storage_path('testing/files');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'sample.pdf';
|
||||
file_put_contents($path, 'PDFDATA');
|
||||
|
||||
$service = new FileServeService();
|
||||
$meta = $service->meta($dir, 'sample.pdf', ['pdf'], 'download');
|
||||
|
||||
$this->assertSame('sample.pdf', $meta['name']);
|
||||
$this->assertSame('download.pdf', $meta['download_name']);
|
||||
$this->assertSame(7, $meta['size']);
|
||||
$this->assertNotEmpty($meta['etag']);
|
||||
}
|
||||
|
||||
public function test_serve_inline_returns_304_when_etag_matches(): void
|
||||
{
|
||||
$dir = storage_path('testing/files');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'cached.pdf';
|
||||
file_put_contents($path, 'PDFDATA');
|
||||
|
||||
$service = new FileServeService();
|
||||
$meta = $service->meta($dir, 'cached.pdf', ['pdf']);
|
||||
|
||||
$request = Request::create('/files/cached.pdf', 'GET', [], [], [], [
|
||||
'HTTP_IF_NONE_MATCH' => $meta['etag'],
|
||||
]);
|
||||
|
||||
$response = $service->serveInline($dir, 'cached.pdf', ['pdf'], $request);
|
||||
|
||||
$this->assertSame(304, $response->getStatusCode());
|
||||
$this->assertSame($meta['etag'], $response->headers->get('ETag'));
|
||||
}
|
||||
|
||||
public function test_serve_inline_rejects_invalid_extension(): void
|
||||
{
|
||||
$this->expectException(HttpException::class);
|
||||
|
||||
$dir = storage_path('testing/files');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'sample.exe';
|
||||
file_put_contents($path, 'DATA');
|
||||
|
||||
$service = new FileServeService();
|
||||
$service->meta($dir, 'sample.exe', ['pdf']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Finance;
|
||||
|
||||
use App\Services\Finance\FinancialChartService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FinancialChartServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_generate_charts_returns_null_in_tests(): void
|
||||
{
|
||||
$service = new FinancialChartService();
|
||||
|
||||
$summary = [
|
||||
'totalCharges' => 100,
|
||||
'amountCollected' => 80,
|
||||
'totalUnpaid' => 20,
|
||||
'totalDiscounts' => 5,
|
||||
'totalRefunds' => 2,
|
||||
'totalExpenses' => 10,
|
||||
'totalReimbursements' => 4,
|
||||
'netAmount' => 93,
|
||||
];
|
||||
|
||||
$this->assertNull($service->generateBarChart($summary));
|
||||
$this->assertNull($service->generatePieChart($summary));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Finance;
|
||||
|
||||
use App\Services\Finance\FinancialPaymentService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FinancialPaymentServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_payment_aggregates_exclude_void_statuses(): void
|
||||
{
|
||||
DB::table('payments')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 30,
|
||||
'balance' => 70,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'Cash',
|
||||
'payment_date' => '2025-01-05',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Paid',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 20,
|
||||
'balance' => 50,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'Credit Card',
|
||||
'payment_date' => '2025-01-06',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Paid',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'parent_id' => 10,
|
||||
'invoice_id' => 1,
|
||||
'total_amount' => 100,
|
||||
'paid_amount' => 50,
|
||||
'balance' => 0,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'Cash',
|
||||
'payment_date' => '2025-01-07',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'void',
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'parent_id' => 11,
|
||||
'invoice_id' => 2,
|
||||
'total_amount' => 40,
|
||||
'paid_amount' => 10,
|
||||
'balance' => 30,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'Check',
|
||||
'payment_date' => '2025-01-08',
|
||||
'school_year' => '2025-2026',
|
||||
'status' => 'Paid',
|
||||
],
|
||||
]);
|
||||
|
||||
$service = new FinancialPaymentService();
|
||||
|
||||
$payments = $service->paymentsByInvoice('2025-2026', '2025-01-01', '2025-01-31');
|
||||
$breakdown = $service->paymentBreakdown('2025-2026', '2025-01-01', '2025-01-31');
|
||||
$totals = $service->paymentTotals('2025-2026', '2025-01-01', '2025-01-31');
|
||||
|
||||
$map = [];
|
||||
foreach ($payments as $row) {
|
||||
$map[(int) $row['invoice_id']] = (float) $row['paid_amount'];
|
||||
}
|
||||
|
||||
$this->assertSame(50.0, $map[1]);
|
||||
$this->assertSame(10.0, $map[2]);
|
||||
|
||||
$this->assertSame(30.0, (float) ($breakdown[1]['cash'] ?? 0));
|
||||
$this->assertSame(20.0, (float) ($breakdown[1]['credit'] ?? 0));
|
||||
$this->assertSame(10.0, (float) ($breakdown[2]['check'] ?? 0));
|
||||
|
||||
$this->assertSame(60.0, $totals['total_all']);
|
||||
$this->assertSame(30.0, $totals['total_cash']);
|
||||
$this->assertSame(20.0, $totals['total_credit']);
|
||||
$this->assertSame(10.0, $totals['total_check']);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user