**Prepared by:** Oditha

**Date:** 23 January 2026

**Version:** 1.0

**Reference:** [FRD - HoReCa Field Operations & Workflow Management](https://www.notion.so/FRD-HoReCa-Field-Operations-Workflow-Management-2f7cd2773bc548c9a2595d856f15ac15?pvs=21)

---

## Overview

This document provides comprehensive unit testing guidelines and test cases for the **System Admin Portal** (Laravel).

---

## Testing Framework

### Tools

-   **PHPUnit:** Built-in Laravel testing framework
-   **Mockery:** Mocking dependencies
-   **Laravel Dusk:** Browser testing
-   **Faker:** Test data generation

---

## Test Structure

```
tests/
├── Unit/
│   ├── Models/
│   │   ├── UserTest.php
│   │   ├── WorkflowTest.php
│   │   └── OutletTest.php
│   ├── Services/
│   │   ├── WorkflowServiceTest.php
│   │   ├── HierarchyServiceTest.php
│   │   └── SyncServiceTest.php
│   └── Jobs/
│       ├── GeneratePOSMAuditsJobTest.php
│       └── FlagOverdueAuditsJobTest.php
├── Feature/
│   ├── Auth/
│   │   └── LoginTest.php
│   ├── Workflows/
│   │   ├── CreateWorkflowTest.php
│   │   ├── UpdateWorkflowTest.php
│   │   └── DeleteWorkflowTest.php
│   ├── Outlets/
│   │   ├── CreateOutletTest.php
│   │   ├── ImportOutletsTest.php
│   │   └── BulkAssignTemplateTest.php
│   ├── ServiceRequests/
│   │   └── UpdateServiceRequestTest.php
│   └── Reports/
│       └── VisitCoverageReportTest.php
└── Browser/
    ├── WorkflowManagementTest.php
    ├── OutletManagementTest.php
    └── DashboardTest.php
```

---

## Unit Tests

### 1. Model Tests

#### Test: User Model

**File:** `tests/Unit/Models/UserTest.php`

```php
<?php

namespace Tests\Unit\Models;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_has_fillable_attributes()
    {
        $fillable = [
            'name',
            'email',
            'password',
            'role',
            'territory_id',
            'region_id',
            'reports_to_id',
            'phone',
            'avatar_url',
            'status',
        ];

        $user = new User();

        $this->assertEquals($fillable, $user->getFillable());
    }

    /** @test */
    public function it_belongs_to_territory()
    {
        $user = User::factory()->create();

        $this->assertInstanceOf(
            \Illuminate\Database\Eloquent\Relations\BelongsTo::class,
            $user->territory()
        );
    }

    /** @test */
    public function it_belongs_to_region()
    {
        $user = User::factory()->create();

        $this->assertInstanceOf(
            \Illuminate\Database\Eloquent\Relations\BelongsTo::class,
            $user->region()
        );
    }

    /** @test */
    public function it_reports_to_another_user()
    {
        $manager = User::factory()->create(['role' => 'tm']);
        $user = User::factory()->create([
            'role' => 'lsr',
            'reports_to_id' => $manager->id,
        ]);

        $this->assertEquals($manager->id, $user->reportsTo->id);
    }

    /** @test */
    public function it_has_subordinates()
    {
        $manager = User::factory()->create(['role' => 'tm']);
        $subordinates = User::factory()->count(3)->create([
            'role' => 'lsr',
            'reports_to_id' => $manager->id,
        ]);

        $this->assertCount(3, $manager->subordinates);
    }

    /** @test */
    public function it_casts_last_login_at_to_datetime()
    {
        $user = User::factory()->create([
            'last_login_at' => '2026-01-23 10:00:00',
        ]);

        $this->assertInstanceOf(\Carbon\Carbon::class, $user->last_login_at);
    }
}
```

---

### 2. Service Tests

#### Test: Hierarchy Service

**File:** `tests/Unit/Services/HierarchyServiceTest.php`

```php
<?php

namespace Tests\Unit\Services;

use App\Models\User;
use App\Services\HierarchyService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HierarchyServiceTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function lsr_can_only_see_own_data()
    {
        $lsr = User::factory()->create(['role' => 'lsr']);

        $userIds = HierarchyService::getUserHierarchy($lsr);

        $this->assertCount(1, $userIds);
        $this->assertContains($lsr->id, $userIds);
    }

    /** @test */
    public function tm_can_see_own_and_subordinate_data()
    {
        $tm = User::factory()->create(['role' => 'tm']);
        $lsr1 = User::factory()->create([
            'role' => 'lsr',
            'reports_to_id' => $tm->id,
        ]);
        $lsr2 = User::factory()->create([
            'role' => 'lsr',
            'reports_to_id' => $tm->id,
        ]);

        $userIds = HierarchyService::getUserHierarchy($tm);

        $this->assertCount(3, $userIds);
        $this->assertContains($tm->id, $userIds);
        $this->assertContains($lsr1->id, $userIds);
        $this->assertContains($lsr2->id, $userIds);
    }

    /** @test */
    public function sm_can_see_tms_and_their_subordinates()
    {
        $sm = User::factory()->create(['role' => 'sm']);
        $tm1 = User::factory()->create([
            'role' => 'tm',
            'reports_to_id' => $sm->id,
        ]);
        $tm2 = User::factory()->create([
            'role' => 'tm',
            'reports_to_id' => $sm->id,
        ]);
        $lsr1 = User::factory()->create([
            'role' => 'lsr',
            'reports_to_id' => $tm1->id,
        ]);
        $lsr2 = User::factory()->create([
            'role' => 'lsr',
            'reports_to_id' => $tm2->id,
        ]);

        $userIds = HierarchyService::getUserHierarchy($sm);

        $this->assertCount(5, $userIds);
        $this->assertContains($sm->id, $userIds);
        $this->assertContains($tm1->id, $userIds);
        $this->assertContains($tm2->id, $userIds);
        $this->assertContains($lsr1->id, $userIds);
        $this->assertContains($lsr2->id, $userIds);
    }

    /** @test */
    public function admin_can_see_all_users()
    {
        $admin = User::factory()->create(['role' => 'admin']);
        User::factory()->count(10)->create();

        $userIds = HierarchyService::getUserHierarchy($admin);

        $this->assertCount(11, $userIds); // 10 + admin
    }
}
```

---

### 3. Job Tests

#### Test: Generate POSM Audits Job

**File:** `tests/Unit/Jobs/GeneratePOSMAuditsJobTest.php`

```php
<?php

namespace Tests\Unit\Jobs;

use App\Jobs\GeneratePOSMAuditsJob;
use App\Models\Outlet;
use App\Models\POSMAudit;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class GeneratePOSMAuditsJobTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_generates_audits_for_outlets_due_20_days_after_last_audit()
    {
        Notification::fake();

        $tm = User::factory()->create(['role' => 'tm']);
        $outlet = Outlet::factory()->create(['assigned_tm_id' => $tm->id]);

        // Create completed audit 20 days ago
        POSMAudit::factory()->create([
            'outlet_id' => $outlet->id,
            'status' => 'completed',
            'completed_date' => Carbon::now()->subDays(20)->toDateString(),
        ]);

        // Execute job
        $job = new GeneratePOSMAuditsJob();
        $job->handle();

        // Assert new audit created
        $this->assertDatabaseHas('posm_audits', [
            'outlet_id' => $outlet->id,
            'assigned_tm_id' => $tm->id,
            'status' => 'scheduled',
            'scheduled_date' => Carbon::now()->toDateString(),
            'due_date' => Carbon::now()->addDays(5)->toDateString(),
        ]);

        // Assert notification sent
        Notification::assertSentTo($tm, \App\Notifications\AuditAssignedNotification::class);
    }

    /** @test */
    public function it_does_not_generate_audits_for_outlets_not_due()
    {
        $tm = User::factory()->create(['role' => 'tm']);
        $outlet = Outlet::factory()->create(['assigned_tm_id' => $tm->id]);

        // Create completed audit 10 days ago (not due yet)
        POSMAudit::factory()->create([
            'outlet_id' => $outlet->id,
            'status' => 'completed',
            'completed_date' => Carbon::now()->subDays(10)->toDateString(),
        ]);

        $auditCountBefore = POSMAudit::count();

        // Execute job
        $job = new GeneratePOSMAuditsJob();
        $job->handle();

        $auditCountAfter = POSMAudit::count();

        // Assert no new audit created
        $this->assertEquals($auditCountBefore, $auditCountAfter);
    }

    /** @test */
    public function it_escalates_to_sm_when_tm_inactive()
    {
        Notification::fake();

        $sm = User::factory()->create(['role' => 'sm']);
        $tm = User::factory()->create([
            'role' => 'tm',
            'status' => 'inactive',
            'reports_to_id' => $sm->id,
        ]);
        $outlet = Outlet::factory()->create([
            'assigned_tm_id' => $tm->id,
            'assigned_sm_id' => $sm->id,
        ]);

        // Create completed audit 20 days ago
        POSMAudit::factory()->create([
            'outlet_id' => $outlet->id,
            'status' => 'completed',
            'completed_date' => Carbon::now()->subDays(20)->toDateString(),
        ]);

        // Execute job
        $job = new GeneratePOSMAuditsJob();
        $job->handle();

        // Assert audit assigned to SM instead of inactive TM
        $this->assertDatabaseHas('posm_audits', [
            'outlet_id' => $outlet->id,
            'assigned_tm_id' => $sm->id,
        ]);
    }
}
```

---

## Feature Tests

### 1. Workflow Management Tests

#### Test: Create Workflow

**File:** `tests/Feature/Workflows/CreateWorkflowTest.php`

```php
<?php

namespace Tests\Feature\Workflows;

use App\Models\User;
use App\Models\Workflow;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CreateWorkflowTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function admin_can_create_workflow()
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)->post('/workflows', [
            'name' => 'Test Workflow',
            'description' => 'Test Description',
            'category' => 'impulse',
            'status' => 'active',
        ]);

        $response->assertRedirect(route('workflows.index'));
        $response->assertSessionHas('success');

        $this->assertDatabaseHas('workflows', [
            'name' => 'Test Workflow',
            'category' => 'impulse',
        ]);
    }

    /** @test */
    public function non_admin_cannot_create_workflow()
    {
        $lsr = User::factory()->create(['role' => 'lsr']);

        $response = $this->actingAs($lsr)->post('/workflows', [
            'name' => 'Test Workflow',
            'category' => 'impulse',
        ]);

        $response->assertForbidden();
    }

    /** @test */
    public function workflow_name_is_required()
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)->post('/workflows', [
            'name' => '',
            'category' => 'impulse',
        ]);

        $response->assertSessionHasErrors('name');
    }

    /** @test */
    public function workflow_name_must_be_unique()
    {
        $admin = User::factory()->create(['role' => 'admin']);
        Workflow::factory()->create(['name' => 'Existing Workflow']);

        $response = $this->actingAs($admin)->post('/workflows', [
            'name' => 'Existing Workflow',
            'category' => 'impulse',
        ]);

        $response->assertSessionHasErrors('name');
    }
}
```

---

### 2. Outlet Management Tests

#### Test: Import Outlets

**File:** `tests/Feature/Outlets/ImportOutletsTest.php`

```php
<?php

namespace Tests\Feature\Outlets;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;

class ImportOutletsTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function admin_can_import_outlets_from_csv()
    {
        Queue::fake();

        $admin = User::factory()->create(['role' => 'admin']);

        // Create CSV content
        $csv = "name,address,latitude,longitude,outlet_type\n";
        $csv .= "Test Outlet 1,123 Main St,6.927079,79.861244,restaurant\n";
        $csv .= "Test Outlet 2,456 Oak Ave,6.930000,79.865000,hotel\n";

        $file = UploadedFile::fake()->createWithContent('outlets.csv', $csv);

        $response = $this->actingAs($admin)->post('/outlets/import', [
            'file' => $file,
        ]);

        $response->assertRedirect();
        $response->assertSessionHas('success');

        Queue::assertPushed(\App\Jobs\ImportOutletsJob::class);
    }

    /** @test */
    public function csv_file_is_required()
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)->post('/outlets/import', [
            'file' => null,
        ]);

        $response->assertSessionHasErrors('file');
    }

    /** @test */
    public function only_csv_files_are_allowed()
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $file = UploadedFile::fake()->create('outlets.txt', 100);

        $response = $this->actingAs($admin)->post('/outlets/import', [
            'file' => $file,
        ]);

        $response->assertSessionHasErrors('file');
    }
}
```

---

### 3. Service Request Tests

#### Test: Update Service Request Status

**File:** `tests/Feature/ServiceRequests/UpdateServiceRequestTest.php`

```php
<?php

namespace Tests\Feature\ServiceRequests;

use App\Models\ServiceRequest;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UpdateServiceRequestTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function assigned_user_can_update_status_to_in_progress()
    {
        $user = User::factory()->create(['role' => 'tm']);
        $sr = ServiceRequest::factory()->create([
            'status' => 'open',
            'assigned_to' => $user->id,
        ]);

        $response = $this->actingAs($user)->post(
            "/service-requests/{$sr->id}/update-status",
            [
                'status' => 'in_progress',
                'comment' => 'Starting work on this',
            ]
        );

        $response->assertRedirect();
        $response->assertSessionHas('success');

        $this->assertDatabaseHas('service_requests', [
            'id' => $sr->id,
            'status' => 'in_progress',
        ]);

        $this->assertDatabaseHas('service_request_activities', [
            'service_request_id' => $sr->id,
            'action_type' => 'status_change',
            'old_value' => 'open',
            'new_value' => 'in_progress',
        ]);
    }

    /** @test */
    public function cannot_update_status_to_resolved_from_open()
    {
        $user = User::factory()->create(['role' => 'tm']);
        $sr = ServiceRequest::factory()->create([
            'status' => 'open',
            'assigned_to' => $user->id,
        ]);

        $response = $this->actingAs($user)->post(
            "/service-requests/{$sr->id}/update-status",
            [
                'status' => 'resolved',
            ]
        );

        $response->assertRedirect();
        $response->assertSessionHas('error');

        $this->assertDatabaseHas('service_requests', [
            'id' => $sr->id,
            'status' => 'open', // Status unchanged
        ]);
    }

    /** @test */
    public function unassigned_user_cannot_update_status()
    {
        $assignedUser = User::factory()->create(['role' => 'tm']);
        $otherUser = User::factory()->create(['role' => 'tm']);
        $sr = ServiceRequest::factory()->create([
            'status' => 'open',
            'assigned_to' => $assignedUser->id,
        ]);

        $response = $this->actingAs($otherUser)->post(
            "/service-requests/{$sr->id}/update-status",
            [
                'status' => 'in_progress',
            ]
        );

        $response->assertForbidden();
    }
}
```

---

## Browser Tests (Dusk)

### Test: Complete Workflow Creation

**File:** `tests/Browser/WorkflowManagementTest.php`

```php
<?php

namespace Tests\Browser;

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class WorkflowManagementTest extends DuskTestCase
{
    use DatabaseMigrations;

    /** @test */
    public function admin_can_create_complete_workflow()
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $this->browse(function (Browser $browser) use ($admin) {
            $browser->loginAs($admin)
                    ->visit('/workflows/create')
                    ->type('name', 'Test Impulse Workflow')
                    ->type('description', 'Testing workflow creation')
                    ->select('category', 'impulse')
                    ->press('Add Step')
                    ->type('steps[0][name]', 'Is display present?')
                    ->select('steps[0][question_type]', 'selection')
                    ->check('steps[0][is_mandatory]')
                    ->check('steps[0][require_photo]')
                    ->press('Add Option')
                    ->type('steps[0][options][0][option_text]', 'Yes')
                    ->type('steps[0][options][0][option_value]', 'yes')
                    ->press('Add Option')
                    ->type('steps[0][options][1][option_text]', 'No')
                    ->type('steps[0][options][1][option_value]', 'no')
                    ->press('Create Workflow')
                    ->assertPathIs('/workflows')
                    ->assertSee('Workflow created successfully')
                    ->assertSee('Test Impulse Workflow');
        });
    }
}
```

---

## Test Execution

### Run All Tests

```bash
php artisan test
```

### Run Specific Test Suite

```bash
php artisan test --testsuite=Unit
php artisan test --testsuite=Feature
```

### Run Specific Test File

```bash
php artisan test tests/Unit/Services/HierarchyServiceTest.php
```

### Run with Coverage

```bash
php artisan test --coverage
```

### Run Dusk Tests

```bash
php artisan dusk
```

---

## Test Coverage Goals

### Minimum Coverage Requirements

-   **Overall:** 80%
-   **Controllers:** 75%
-   **Services:** 95%
-   **Models:** 90%
-   **Jobs:** 90%
-   **Repositories:** 90%

---

**Document Status:** Final

**Next Steps:** Share with backend QA team for test execution
