Initial Ladill Frontdesk release with deploy pipeline.
Visitor management app with SSO, kiosk, badges, reports, and Gitea CI deploy to frontdesk.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected string $apiKey = 'test-frontdesk-api-key';
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['frontdesk.service_api_keys' => ['care' => $this->apiKey]]);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'api-user-001',
|
||||
'name' => 'API User',
|
||||
'email' => 'api@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'API Org',
|
||||
'slug' => 'api-org',
|
||||
'settings' => ['onboarded' => true],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_api_requires_service_key(): void
|
||||
{
|
||||
$this->getJson('/api/visits?owner='.$this->user->public_id)
|
||||
->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_api_can_create_visit(): void
|
||||
{
|
||||
$response = $this->withHeaders(['Authorization' => 'Bearer '.$this->apiKey])
|
||||
->postJson('/api/visits', [
|
||||
'owner' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'API Visitor',
|
||||
'visitor_type' => 'visitor',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$this->assertDatabaseHas('frontdesk_visits', ['visitor_type' => 'visitor']);
|
||||
}
|
||||
|
||||
public function test_api_can_list_visits(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Listed Visitor',
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->apiKey])
|
||||
->getJson('/api/visits?owner='.$this->user->public_id)
|
||||
->assertOk()
|
||||
->assertJsonPath('total', 1);
|
||||
}
|
||||
|
||||
public function test_api_scopes_to_owner(): void
|
||||
{
|
||||
$other = User::create([
|
||||
'public_id' => 'other-user',
|
||||
'name' => 'Other',
|
||||
'email' => 'other@example.com',
|
||||
]);
|
||||
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $other->public_id,
|
||||
'organization_id' => Organization::create([
|
||||
'owner_ref' => $other->public_id,
|
||||
'name' => 'Other Org',
|
||||
'slug' => 'other',
|
||||
])->id,
|
||||
'full_name' => 'Secret Visitor',
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $other->public_id,
|
||||
'organization_id' => $visitor->organization_id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->apiKey])
|
||||
->getJson('/api/visits?owner='.$this->user->public_id)
|
||||
->assertOk()
|
||||
->assertJsonPath('total', 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use App\Models\WebhookEndpoint;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase10Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected string $careKey = 'test-care-api-key';
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['frontdesk.service_api_keys' => ['care' => $this->careKey, 'lab' => 'test-lab-key']]);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase10-user-001',
|
||||
'name' => 'Phase10 User',
|
||||
'email' => 'phase10@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase10 Org',
|
||||
'slug' => 'phase10-org',
|
||||
'settings' => ['onboarded' => true],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_care_api_can_create_visit_with_external_ref(): void
|
||||
{
|
||||
$response = $this->withHeaders(['Authorization' => 'Bearer '.$this->careKey])
|
||||
->postJson('/api/visits', [
|
||||
'owner' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'external_ref' => 'care-appt-001',
|
||||
'full_name' => 'Care Patient',
|
||||
'visitor_type' => 'visitor',
|
||||
'integration_metadata' => ['appointment_id' => 'A-100'],
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$this->assertDatabaseHas('frontdesk_visits', [
|
||||
'external_ref' => 'care-appt-001',
|
||||
'source' => 'care',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_external_ref_is_idempotent_for_same_source(): void
|
||||
{
|
||||
$payload = [
|
||||
'owner' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'external_ref' => 'lab-specimen-42',
|
||||
'full_name' => 'Lab Walk-in',
|
||||
'visitor_type' => 'visitor',
|
||||
];
|
||||
|
||||
config(['frontdesk.service_api_keys' => ['lab' => 'test-lab-key']]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer test-lab-key'])
|
||||
->postJson('/api/visits', $payload)
|
||||
->assertCreated();
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer test-lab-key'])
|
||||
->postJson('/api/visits', $payload)
|
||||
->assertOk();
|
||||
|
||||
$this->assertSame(1, Visit::where('external_ref', 'lab-specimen-42')->count());
|
||||
}
|
||||
|
||||
public function test_webhook_dispatched_on_check_in(): void
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
WebhookEndpoint::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'url' => 'https://example.com/webhooks/frontdesk',
|
||||
'secret' => 'secret123',
|
||||
'events' => ['visit.checked_in'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->careKey])
|
||||
->postJson('/api/visits', [
|
||||
'owner' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Webhook Guest',
|
||||
'visitor_type' => 'visitor',
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
Http::assertSent(fn ($request) => $request->url() === 'https://example.com/webhooks/frontdesk');
|
||||
}
|
||||
|
||||
public function test_ical_feed_returns_calendar_data(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Scheduled Guest',
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_EXPECTED,
|
||||
'scheduled_at' => now()->addHours(2),
|
||||
]);
|
||||
|
||||
$token = hash_hmac('sha256', "{$this->organization->id}:{$this->user->public_id}", (string) config('app.key'));
|
||||
|
||||
$this->get(route('frontdesk.integrations.ical', [
|
||||
'organization' => $this->organization->id,
|
||||
'owner' => $this->user->public_id,
|
||||
'token' => $token,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/calendar; charset=utf-8')
|
||||
->assertSee('BEGIN:VCALENDAR');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Device;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase11Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase11-user-001',
|
||||
'name' => 'Phase11 User',
|
||||
'email' => 'phase11@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase11 Org',
|
||||
'slug' => 'phase11-org',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_dashboard_stats_are_cached(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
$this->actingAs($this->user)->get(route('frontdesk.dashboard'))->assertOk();
|
||||
$this->assertTrue(Cache::has('fd:dashboard:'.$this->user->public_id.':'.$this->organization->id.':all'));
|
||||
}
|
||||
|
||||
public function test_kiosk_device_route_is_rate_limited(): void
|
||||
{
|
||||
RateLimiter::clear('kiosk-device');
|
||||
|
||||
$device = Device::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Limited Kiosk',
|
||||
'type' => 'kiosk',
|
||||
'device_token' => 'rate-limit-token',
|
||||
'status' => 'offline',
|
||||
]);
|
||||
|
||||
for ($i = 0; $i < 31; $i++) {
|
||||
$response = $this->postJson(route('frontdesk.kiosk.device.check-in', $device->device_token), [
|
||||
'full_name' => 'Guest '.$i,
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$response->assertStatus(429);
|
||||
}
|
||||
|
||||
public function test_offline_sync_replays_queued_check_in(): void
|
||||
{
|
||||
$device = Device::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => Branch::first()->id,
|
||||
'name' => 'Offline Kiosk',
|
||||
'type' => 'kiosk',
|
||||
'device_token' => 'offline-sync-token',
|
||||
'status' => 'online',
|
||||
]);
|
||||
|
||||
$clientId = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
$this->postJson('/api/kiosk/offline-sync', [
|
||||
'client_id' => $clientId,
|
||||
'payload' => [
|
||||
'full_name' => 'Offline Guest',
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => true,
|
||||
],
|
||||
], ['X-Device-Token' => $device->device_token])
|
||||
->assertCreated()
|
||||
->assertJsonPath('status', 'synced');
|
||||
|
||||
$this->postJson('/api/kiosk/offline-sync', [
|
||||
'client_id' => $clientId,
|
||||
'payload' => [
|
||||
'full_name' => 'Offline Guest',
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => true,
|
||||
],
|
||||
], ['X-Device-Token' => $device->device_token])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'already_synced');
|
||||
}
|
||||
|
||||
public function test_app_layout_supports_dark_mode_toggle_markup(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.dashboard'))
|
||||
->assertOk()
|
||||
->assertSee('Toggle dark mode', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase2Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $admin;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->admin = User::create([
|
||||
'public_id' => 'admin-001',
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->admin->public_id,
|
||||
'name' => 'Phase2 Org',
|
||||
'slug' => 'phase2-org',
|
||||
'settings' => ['onboarded' => true],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->admin->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->admin->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_onboarding_creates_organization_and_branch(): void
|
||||
{
|
||||
Organization::query()->delete();
|
||||
Member::query()->delete();
|
||||
|
||||
$this->actingAs($this->admin)
|
||||
->post(route('frontdesk.onboarding.store'), [
|
||||
'organization_name' => 'New Corp',
|
||||
'branch_name' => 'HQ',
|
||||
'timezone' => 'UTC',
|
||||
'badge_expiry_hours' => 8,
|
||||
])
|
||||
->assertRedirect(route('frontdesk.dashboard'));
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_organizations', ['name' => 'New Corp']);
|
||||
$this->assertDatabaseHas('frontdesk_branches', ['name' => 'HQ']);
|
||||
$this->assertDatabaseHas('frontdesk_members', ['role' => 'org_admin']);
|
||||
}
|
||||
|
||||
public function test_org_admin_can_create_branch(): void
|
||||
{
|
||||
$this->actingAs($this->admin)
|
||||
->post(route('frontdesk.branches.store'), [
|
||||
'name' => 'East Wing',
|
||||
'address' => '123 Main St',
|
||||
])
|
||||
->assertRedirect(route('frontdesk.branches.index'));
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_branches', ['name' => 'East Wing']);
|
||||
}
|
||||
|
||||
public function test_org_admin_can_add_team_member(): void
|
||||
{
|
||||
$branch = Branch::create([
|
||||
'owner_ref' => $this->admin->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin)
|
||||
->post(route('frontdesk.members.store'), [
|
||||
'user_ref' => 'receptionist-001',
|
||||
'role' => 'receptionist',
|
||||
'branch_id' => $branch->id,
|
||||
])
|
||||
->assertRedirect(route('frontdesk.members.index'));
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_members', [
|
||||
'user_ref' => 'receptionist-001',
|
||||
'role' => 'receptionist',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_receptionist_cannot_manage_branches(): void
|
||||
{
|
||||
$receptionist = User::create([
|
||||
'public_id' => 'receptionist-001',
|
||||
'name' => 'Receptionist',
|
||||
'email' => 'rec@example.com',
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->admin->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $receptionist->public_id,
|
||||
'role' => 'receptionist',
|
||||
]);
|
||||
|
||||
$this->actingAs($receptionist)
|
||||
->get(route('frontdesk.branches.create'))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_receptionist_can_access_check_in(): void
|
||||
{
|
||||
$receptionist = User::create([
|
||||
'public_id' => 'receptionist-002',
|
||||
'name' => 'Receptionist Two',
|
||||
'email' => 'rec2@example.com',
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->admin->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $receptionist->public_id,
|
||||
'role' => 'receptionist',
|
||||
]);
|
||||
|
||||
$this->actingAs($receptionist)
|
||||
->get(route('frontdesk.visits.create'))
|
||||
->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Host;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase3Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Branch $branch;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase3-user-001',
|
||||
'name' => 'Phase3 User',
|
||||
'email' => 'phase3@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase3 Org',
|
||||
'slug' => 'phase3-org',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_schedule_future_visit(): void
|
||||
{
|
||||
$scheduledAt = now()->addDay()->format('Y-m-d\TH:i');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.schedule.store'), [
|
||||
'full_name' => 'Future Guest',
|
||||
'email' => 'future@example.com',
|
||||
'visitor_type' => 'visitor',
|
||||
'scheduled_at' => $scheduledAt,
|
||||
'purpose' => 'Meeting',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_visits', [
|
||||
'status' => Visit::STATUS_SCHEDULED,
|
||||
'purpose' => 'Meeting',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_schedule_today_visit_as_expected(): void
|
||||
{
|
||||
$scheduledAt = now()->addHour()->format('Y-m-d\TH:i');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.schedule.store'), [
|
||||
'full_name' => 'Today Guest',
|
||||
'email' => 'today@example.com',
|
||||
'visitor_type' => 'visitor',
|
||||
'scheduled_at' => $scheduledAt,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_visits', [
|
||||
'status' => Visit::STATUS_EXPECTED,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_check_in_scheduled_visit(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Scheduled Guest',
|
||||
'email' => 'scheduled@example.com',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_EXPECTED,
|
||||
'scheduled_at' => now()->addHour(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.activate', $visit), [
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect(route('frontdesk.visits.show', $visit));
|
||||
|
||||
$visit->refresh();
|
||||
$this->assertSame(Visit::STATUS_CHECKED_IN, $visit->status);
|
||||
$this->assertNotNull($visit->checked_in_at);
|
||||
}
|
||||
|
||||
public function test_can_mark_visit_waiting_and_cancel(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Waiting Guest',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_EXPECTED,
|
||||
'scheduled_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.waiting', $visit))
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertSame(Visit::STATUS_WAITING, $visit->fresh()->status);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.cancel', $visit), ['reason' => 'No show'])
|
||||
->assertRedirect(route('frontdesk.visits.index', ['status' => 'cancelled']));
|
||||
|
||||
$this->assertSame(Visit::STATUS_CANCELLED, $visit->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_returning_visitor_quick_check_in(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Regular Guest',
|
||||
'visit_count' => 3,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visitors.check-in.store', $visitor), [
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_visits', [
|
||||
'visitor_id' => $visitor->id,
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_update_visitor_profile(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Profile Guest',
|
||||
'notes' => 'Old note',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->patch(route('frontdesk.visitors.update', $visitor), [
|
||||
'full_name' => 'Updated Guest',
|
||||
'company' => 'Acme Corp',
|
||||
'notes' => 'VIP guest',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$visitor->refresh();
|
||||
$this->assertSame('Updated Guest', $visitor->full_name);
|
||||
$this->assertSame('Acme Corp', $visitor->company);
|
||||
$this->assertSame('VIP guest', $visitor->notes);
|
||||
}
|
||||
|
||||
public function test_mark_overdue_command_updates_past_visits(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-06-27 12:00:00');
|
||||
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Late Guest',
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_EXPECTED,
|
||||
'scheduled_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
$this->artisan('frontdesk:mark-overdue-visits')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_visits', [
|
||||
'visitor_id' => $visitor->id,
|
||||
'status' => Visit::STATUS_OVERDUE,
|
||||
]);
|
||||
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
|
||||
public function test_calendar_page_loads(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.visits.calendar'))
|
||||
->assertOk()
|
||||
->assertSee('Visit calendar');
|
||||
}
|
||||
|
||||
public function test_api_can_schedule_visit(): void
|
||||
{
|
||||
config(['frontdesk.service_api_keys.crm' => 'test-api-key']);
|
||||
|
||||
$scheduledAt = now()->addDays(2)->toIso8601String();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer test-api-key')
|
||||
->postJson('/api/visits?owner='.$this->user->public_id, [
|
||||
'schedule' => true,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'API Guest',
|
||||
'email' => 'api@example.com',
|
||||
'visitor_type' => 'visitor',
|
||||
'scheduled_at' => $scheduledAt,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('status', Visit::STATUS_SCHEDULED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase4Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase4-user-001',
|
||||
'name' => 'Phase4 User',
|
||||
'email' => 'phase4@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase4 Org',
|
||||
'slug' => 'phase4-org',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_contractor_check_in_stores_details_and_extended_badge(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Site Worker',
|
||||
'visitor_type' => 'contractor',
|
||||
'policies_accepted' => '1',
|
||||
'type_fields' => [
|
||||
'contract_company' => 'BuildCo',
|
||||
'supervisor' => 'Jane Smith',
|
||||
'safety_induction_completed' => '1',
|
||||
'permit_number' => 'PERMIT-99',
|
||||
],
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertSame(Visit::STATUS_CHECKED_IN, $visit->status);
|
||||
$this->assertSame('BuildCo', $visit->contractor_details['contract_company']);
|
||||
$this->assertSame('PERMIT-99', $visit->contractor_details['permit_number']);
|
||||
$this->assertTrue($visit->badge_expires_at->greaterThan(now()->addHours(11)));
|
||||
}
|
||||
|
||||
public function test_delivery_check_in_records_received_time(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Courier',
|
||||
'visitor_type' => 'delivery',
|
||||
'policies_accepted' => '1',
|
||||
'type_fields' => [
|
||||
'courier_company' => 'FastPost',
|
||||
'recipient' => 'Finance Dept',
|
||||
'tracking_number' => 'TRK123',
|
||||
],
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertSame('FastPost', $visit->delivery_details['courier_company']);
|
||||
$this->assertArrayHasKey('received_at', $visit->delivery_details);
|
||||
$this->assertTrue($visit->badge_expires_at->lessThanOrEqualTo(now()->addHours(2)->addMinute()));
|
||||
}
|
||||
|
||||
public function test_vendor_check_in_requires_approval(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Vendor Rep',
|
||||
'visitor_type' => 'vendor',
|
||||
'policies_accepted' => '1',
|
||||
'type_fields' => [
|
||||
'vendor_company' => 'Supply Ltd',
|
||||
'service_type' => 'Office supplies',
|
||||
],
|
||||
])
|
||||
->assertRedirect()
|
||||
->assertSessionHas('success', 'Visit submitted for approval.');
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertSame(Visit::STATUS_WAITING, $visit->status);
|
||||
$this->assertNull($visit->checked_in_at);
|
||||
$this->assertTrue($visit->awaitingApproval());
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.approve', $visit))
|
||||
->assertRedirect();
|
||||
|
||||
$visit->refresh();
|
||||
$this->assertSame(Visit::STATUS_CHECKED_IN, $visit->status);
|
||||
$this->assertNotNull($visit->checked_in_at);
|
||||
}
|
||||
|
||||
public function test_kiosk_contractor_check_in_via_json(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->postJson(route('frontdesk.kiosk.check-in'), [
|
||||
'full_name' => 'Kiosk Contractor',
|
||||
'visitor_type' => 'contractor',
|
||||
'policies_accepted' => true,
|
||||
'type_fields' => [
|
||||
'contract_company' => 'Kiosk Build',
|
||||
'supervisor' => 'Supervisor',
|
||||
'safety_induction_completed' => true,
|
||||
],
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('visit.status', Visit::STATUS_CHECKED_IN);
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_visits', [
|
||||
'visitor_type' => 'contractor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_org_can_override_contractor_badge_expiry(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->put(route('frontdesk.settings.update'), [
|
||||
'name' => 'Phase4 Org',
|
||||
'timezone' => 'UTC',
|
||||
'badge_expiry_hours' => 8,
|
||||
'kiosk_reset_seconds' => 120,
|
||||
'contractor_badge_expiry_hours' => 16,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Long Shift Worker',
|
||||
'visitor_type' => 'contractor',
|
||||
'policies_accepted' => '1',
|
||||
'type_fields' => [
|
||||
'contract_company' => 'BuildCo',
|
||||
'supervisor' => 'Jane Smith',
|
||||
'safety_induction_completed' => '1',
|
||||
],
|
||||
]);
|
||||
|
||||
$visit = Visit::latest()->first();
|
||||
$this->assertTrue($visit->badge_expires_at->greaterThan(now()->addHours(15)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use App\Models\WatchlistEntry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase5Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase5-user-001',
|
||||
'name' => 'Phase5 User',
|
||||
'email' => 'phase5@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase5 Org',
|
||||
'slug' => 'phase5-org',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_blacklisted_check_in_records_audit_log(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Blocked Person',
|
||||
'email' => 'blocked@example.com',
|
||||
'watchlist_status' => Visitor::WATCHLIST_BLACKLISTED,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'visitor_id' => $visitor->id,
|
||||
'full_name' => 'Blocked Person',
|
||||
'email' => 'blocked@example.com',
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_audit_logs', [
|
||||
'action' => 'watchlist.blocked_attempt',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_flagged_visitor_check_in_queues_for_approval(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Flagged Person',
|
||||
'watchlist_status' => Visitor::WATCHLIST_REQUIRES_APPROVAL,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visitors.check-in.store', $visitor), [
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertSame(Visit::STATUS_WAITING, $visit->status);
|
||||
$this->assertTrue($visit->awaitingApproval());
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_audit_logs', [
|
||||
'action' => 'watchlist.flagged_checkin',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_watchlist_admin_can_create_entry(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.watchlist.store'), [
|
||||
'full_name' => 'Known Person',
|
||||
'status' => Visitor::WATCHLIST_BLACKLISTED,
|
||||
'reason' => 'Previous incident',
|
||||
])
|
||||
->assertRedirect(route('frontdesk.watchlist.index'));
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_watchlist_entries', [
|
||||
'full_name' => 'Known Person',
|
||||
'status' => Visitor::WATCHLIST_BLACKLISTED,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_audit_log_page_loads_and_exports_csv(): void
|
||||
{
|
||||
AuditLog::record(
|
||||
$this->user->public_id,
|
||||
'visit.checked_in',
|
||||
$this->organization->id,
|
||||
$this->user->public_id,
|
||||
Visit::class,
|
||||
1,
|
||||
['visitor' => 'Test'],
|
||||
);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.audit.index'))
|
||||
->assertOk()
|
||||
->assertSee('Audit log');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.audit.export'))
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
}
|
||||
|
||||
public function test_security_can_verify_badge_code(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Verify Me',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'badge_code' => 'ABC12345',
|
||||
'checked_in_at' => now(),
|
||||
'badge_expires_at' => now()->addHours(4),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.security.verify.lookup'), ['lookup' => 'ABC12345'])
|
||||
->assertOk()
|
||||
->assertSee('Verify Me')
|
||||
->assertSee('Valid');
|
||||
}
|
||||
|
||||
public function test_soft_deleted_visitor_can_be_restored(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Archived Person',
|
||||
]);
|
||||
|
||||
$visitor->delete();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.compliance.visitors.restore', $visitor->id))
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertNull($visitor->fresh()->deleted_at);
|
||||
$this->assertDatabaseHas('frontdesk_audit_logs', ['action' => 'visitor.restored']);
|
||||
}
|
||||
|
||||
public function test_expired_badge_command_logs_alert(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-06-27 18:00:00');
|
||||
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Expired Badge Guest',
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now()->subHours(10),
|
||||
'badge_expires_at' => now()->subHour(),
|
||||
'badge_code' => 'EXP12345',
|
||||
]);
|
||||
|
||||
$this->artisan('frontdesk:mark-expired-badges')->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('frontdesk_audit_logs', ['action' => 'badge.expired']);
|
||||
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Device;
|
||||
use App\Models\Host;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use App\Notifications\FrontdeskAlertNotification;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase6Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Host $host;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase6-user-001',
|
||||
'name' => 'Phase6 User',
|
||||
'email' => 'phase6@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase6 Org',
|
||||
'slug' => 'phase6-org',
|
||||
'settings' => [
|
||||
'onboarded' => true,
|
||||
'badge_expiry_hours' => 8,
|
||||
'notification_channels' => ['email'],
|
||||
'notification_events' => config('frontdesk.default_notification_events'),
|
||||
],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'host',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->host = Host::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Phase6 Host',
|
||||
'email' => 'host@example.com',
|
||||
'user_ref' => $this->user->public_id,
|
||||
'is_available' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_host_portal_lists_linked_profile(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.host.index'))
|
||||
->assertOk()
|
||||
->assertSee('Phase6 Host');
|
||||
}
|
||||
|
||||
public function test_host_can_pre_register_visitor(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.host.schedule.store'), [
|
||||
'full_name' => 'Expected Guest',
|
||||
'visitor_type' => 'visitor',
|
||||
'scheduled_at' => now()->addDay()->format('Y-m-d\TH:i'),
|
||||
'purpose' => 'Project review',
|
||||
])
|
||||
->assertRedirect(route('frontdesk.host.index'));
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertSame($this->host->id, $visit->host_id);
|
||||
$this->assertSame('Expected Guest', $visit->visitor->full_name);
|
||||
$this->assertSame(Visit::STATUS_SCHEDULED, $visit->status);
|
||||
}
|
||||
|
||||
public function test_host_can_approve_pending_visit(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Vendor Rep',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'host_id' => $this->host->id,
|
||||
'visitor_type' => 'vendor',
|
||||
'status' => Visit::STATUS_WAITING,
|
||||
'policies_accepted' => true,
|
||||
'contractor_details' => [
|
||||
'vendor_company' => 'Acme',
|
||||
'service_type' => 'IT',
|
||||
'_awaiting_approval' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.host.approve', $visit))
|
||||
->assertRedirect(route('frontdesk.host.index'));
|
||||
|
||||
$this->assertSame(Visit::STATUS_CHECKED_IN, $visit->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_check_in_notifies_linked_host_via_database(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$admin = User::create([
|
||||
'public_id' => 'phase6-admin-001',
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@example.com',
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $admin->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Walk-in Guest',
|
||||
'host_id' => $this->host->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
Notification::assertSentTo($this->user, FrontdeskAlertNotification::class);
|
||||
}
|
||||
|
||||
public function test_disabled_notification_event_skips_host_alert(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'notification_events' => array_merge(
|
||||
config('frontdesk.default_notification_events'),
|
||||
['visitor_arrived' => false],
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
$admin = User::create([
|
||||
'public_id' => 'phase6-admin-002',
|
||||
'name' => 'Admin Two',
|
||||
'email' => 'admin2@example.com',
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $admin->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Silent Guest',
|
||||
'host_id' => $this->host->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
Notification::assertNotSentTo($this->user, FrontdeskAlertNotification::class);
|
||||
}
|
||||
|
||||
public function test_host_can_toggle_availability(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.host.availability'))
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertFalse($this->host->fresh()->is_available);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Device;
|
||||
use App\Models\Host;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase7Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected string $deviceToken;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase7-user-001',
|
||||
'name' => 'Phase7 User',
|
||||
'email' => 'phase7@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase7 Org',
|
||||
'slug' => 'phase7-org',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Host::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Lobby Host',
|
||||
'is_available' => true,
|
||||
]);
|
||||
|
||||
$this->deviceToken = 'kiosk-test-token-'.str_repeat('x', 32);
|
||||
}
|
||||
|
||||
public function test_admin_can_register_kiosk_device(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.devices.store'), [
|
||||
'name' => 'Lobby Kiosk',
|
||||
'type' => 'kiosk',
|
||||
])
|
||||
->assertRedirect(route('frontdesk.devices.index'));
|
||||
|
||||
$device = Device::first();
|
||||
$this->assertSame('kiosk', $device->type);
|
||||
$this->assertNotEmpty($device->device_token);
|
||||
}
|
||||
|
||||
public function test_kiosk_device_page_works_without_auth(): void
|
||||
{
|
||||
$device = $this->createKioskDevice();
|
||||
|
||||
$this->get(route('frontdesk.kiosk.device', $device->device_token))
|
||||
->assertOk()
|
||||
->assertSee('Visitor Check-in');
|
||||
|
||||
$this->assertTrue($device->fresh()->isOnline());
|
||||
}
|
||||
|
||||
public function test_kiosk_device_can_check_in_visitor(): void
|
||||
{
|
||||
$device = $this->createKioskDevice();
|
||||
$host = Host::first();
|
||||
|
||||
$this->postJson(route('frontdesk.kiosk.device.check-in', $device->device_token), [
|
||||
'full_name' => 'Kiosk Guest',
|
||||
'host_id' => $host->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => 1,
|
||||
])->assertOk()
|
||||
->assertJsonPath('visit.visitor_name', 'Kiosk Guest');
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertSame(Visit::STATUS_CHECKED_IN, $visit->status);
|
||||
$this->assertSame($device->branch_id, $visit->branch_id);
|
||||
}
|
||||
|
||||
public function test_device_heartbeat_api_marks_online(): void
|
||||
{
|
||||
$device = $this->createKioskDevice(['status' => 'offline', 'last_online_at' => null]);
|
||||
|
||||
$this->postJson('/api/devices/heartbeat', [], [
|
||||
'X-Device-Token' => $device->device_token,
|
||||
])->assertOk()
|
||||
->assertJsonPath('status', 'online');
|
||||
|
||||
$this->assertTrue($device->fresh()->isOnline());
|
||||
}
|
||||
|
||||
public function test_mark_devices_offline_command(): void
|
||||
{
|
||||
Carbon::setTestNow(now());
|
||||
|
||||
$device = $this->createKioskDevice([
|
||||
'status' => 'online',
|
||||
'last_online_at' => now()->subMinutes(30),
|
||||
]);
|
||||
|
||||
$this->artisan('frontdesk:mark-devices-offline')->assertSuccessful();
|
||||
|
||||
$this->assertSame('offline', $device->fresh()->status);
|
||||
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
|
||||
public function test_invalid_device_token_returns_not_found(): void
|
||||
{
|
||||
$this->get(route('frontdesk.kiosk.device', 'invalid-token'))
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $overrides */
|
||||
protected function createKioskDevice(array $overrides = []): Device
|
||||
{
|
||||
return Device::create(array_merge([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => Branch::first()->id,
|
||||
'name' => 'Test Kiosk',
|
||||
'type' => 'kiosk',
|
||||
'status' => 'offline',
|
||||
'device_token' => $this->deviceToken,
|
||||
'config' => ['mode' => 'self_service'],
|
||||
], $overrides));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use App\Services\Frontdesk\VisitorTypeService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase8Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('public');
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase8-user-001',
|
||||
'name' => 'Phase8 User',
|
||||
'email' => 'phase8@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase8 Org',
|
||||
'slug' => 'phase8-org',
|
||||
'settings' => [
|
||||
'onboarded' => true,
|
||||
'badge_expiry_hours' => 8,
|
||||
'badge_template' => ['show_qr' => true, 'show_photo' => true],
|
||||
],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_badge_template_settings_can_be_updated(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->put(route('frontdesk.settings.badge.update'), [
|
||||
'show_photo' => '1',
|
||||
'show_qr' => '1',
|
||||
'show_host' => '1',
|
||||
'show_company' => '0',
|
||||
'show_type' => '1',
|
||||
'primary_color' => '#ff0000',
|
||||
'footer_text' => 'Property of Phase8 Org',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$settings = $this->organization->fresh()->settings;
|
||||
$this->assertSame('#ff0000', $settings['badge_template']['primary_color']);
|
||||
$this->assertFalse($settings['badge_template']['show_company']);
|
||||
}
|
||||
|
||||
public function test_check_in_stores_photo_from_base64(): void
|
||||
{
|
||||
$png = 'data:image/png;base64,'.base64_encode(UploadedFile::fake()->image('photo.png')->getContent());
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'Photo Guest',
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
'photo_data' => $png,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$visit = Visit::first();
|
||||
$this->assertNotNull($visit->photo_path);
|
||||
Storage::disk('public')->assertExists($visit->photo_path);
|
||||
}
|
||||
|
||||
public function test_badge_preview_renders_for_checked_in_visit(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Badge Guest',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now(),
|
||||
'badge_code' => 'ABC123',
|
||||
'qr_token' => 'qr-token-123',
|
||||
'badge_expires_at' => now()->addHours(8),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.visits.badge.preview', $visit))
|
||||
->assertOk()
|
||||
->assertSee('Badge Guest')
|
||||
->assertSee('ABC123');
|
||||
}
|
||||
|
||||
public function test_evacuation_badges_page_lists_current_visitors(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Inside Guest',
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now(),
|
||||
'badge_code' => 'EVAC1',
|
||||
'qr_token' => 'evac-token',
|
||||
'badge_expires_at' => now()->addHours(8),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.security.evacuation.badges'))
|
||||
->assertOk()
|
||||
->assertSee('Inside Guest');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskPhase9Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'phase9-user-001',
|
||||
'name' => 'Phase9 User',
|
||||
'email' => 'phase9@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Phase9 Org',
|
||||
'slug' => 'phase9-org',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'HQ',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_reports_page_loads_with_summary(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Report Guest',
|
||||
'is_frequent' => true,
|
||||
'visit_count' => 6,
|
||||
]);
|
||||
|
||||
Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now(),
|
||||
'checked_out_at' => now()->addHour(),
|
||||
'badge_code' => 'RPT1',
|
||||
'qr_token' => 'rpt-token',
|
||||
'badge_expires_at' => now()->addHours(8),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.reports.index'))
|
||||
->assertOk()
|
||||
->assertSee('Reports')
|
||||
->assertSee('Report Guest');
|
||||
}
|
||||
|
||||
public function test_reports_can_export_csv(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.reports.export'))
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
}
|
||||
|
||||
public function test_daily_report_command_runs(): void
|
||||
{
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'report_daily_recipients' => ['admin@example.com'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->artisan('frontdesk:send-daily-reports')->assertSuccessful();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Host;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\Visitor;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FrontdeskWebTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'test-user-001',
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Test Org',
|
||||
'slug' => 'test-org',
|
||||
'timezone' => 'UTC',
|
||||
'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8, 'kiosk_reset_seconds' => 120],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'org_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main Office',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_to_sso(): void
|
||||
{
|
||||
$this->get('/dashboard')->assertRedirect();
|
||||
}
|
||||
|
||||
public function test_unonboarded_user_is_redirected_to_onboarding(): void
|
||||
{
|
||||
Organization::query()->delete();
|
||||
Member::query()->delete();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.dashboard'))
|
||||
->assertRedirect(route('frontdesk.onboarding.show'));
|
||||
}
|
||||
|
||||
public function test_dashboard_loads_for_authenticated_user(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->get(route('frontdesk.dashboard'))
|
||||
->assertOk()
|
||||
->assertSee('Reception dashboard');
|
||||
}
|
||||
|
||||
public function test_receptionist_can_check_in_visitor(): void
|
||||
{
|
||||
$host = Host::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Jane Host',
|
||||
'email' => 'jane@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('frontdesk.visits.store'), [
|
||||
'full_name' => 'John Visitor',
|
||||
'company' => 'Acme Corp',
|
||||
'phone' => '+233201234567',
|
||||
'email' => 'john@example.com',
|
||||
'host_id' => $host->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'purpose' => 'Meeting',
|
||||
'policies_accepted' => '1',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('frontdesk_visitors', ['full_name' => 'John Visitor']);
|
||||
$this->assertDatabaseHas('frontdesk_visits', ['status' => Visit::STATUS_CHECKED_IN]);
|
||||
}
|
||||
|
||||
public function test_receptionist_can_check_out_visitor(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'John Visitor',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'visitor_id' => $visitor->id,
|
||||
'visitor_type' => 'visitor',
|
||||
'status' => Visit::STATUS_CHECKED_IN,
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('frontdesk.visits.checkout', $visit))
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertEquals(Visit::STATUS_CHECKED_OUT, $visit->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_blacklisted_visitor_cannot_check_in(): void
|
||||
{
|
||||
$visitor = Visitor::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'full_name' => 'Blocked Person',
|
||||
'watchlist_status' => Visitor::WATCHLIST_BLACKLISTED,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)->post(route('frontdesk.visits.store'), [
|
||||
'visitor_id' => $visitor->id,
|
||||
'full_name' => 'Blocked Person',
|
||||
'visitor_type' => 'visitor',
|
||||
'policies_accepted' => '1',
|
||||
])->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_org_admin_can_update_settings(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->put(route('frontdesk.settings.update'), [
|
||||
'name' => 'Updated Org',
|
||||
'timezone' => 'Africa/Accra',
|
||||
'badge_expiry_hours' => 6,
|
||||
'kiosk_reset_seconds' => 90,
|
||||
'notification_channels' => ['email'],
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->organization->refresh();
|
||||
$this->assertEquals('Updated Org', $this->organization->name);
|
||||
$this->assertEquals(6, $this->organization->settings['badge_expiry_hours']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutVite();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\Visitor;
|
||||
use App\Services\Frontdesk\WatchlistService;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WatchlistServiceTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function blacklisted_visitor_is_blocked(): void
|
||||
{
|
||||
$visitor = new Visitor(['watchlist_status' => Visitor::WATCHLIST_BLACKLISTED]);
|
||||
|
||||
$this->expectException(HttpException::class);
|
||||
app(WatchlistService::class)->assertCanCheckIn($visitor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function allowed_visitor_passes_watchlist_check(): void
|
||||
{
|
||||
$visitor = new Visitor(['watchlist_status' => Visitor::WATCHLIST_ALLOWED]);
|
||||
|
||||
app(WatchlistService::class)->assertCanCheckIn($visitor);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function requires_approval_visitor_is_not_blocked_at_gate(): void
|
||||
{
|
||||
$visitor = new Visitor(['watchlist_status' => Visitor::WATCHLIST_REQUIRES_APPROVAL]);
|
||||
|
||||
app(WatchlistService::class)->assertCanCheckIn($visitor);
|
||||
$this->assertTrue(app(WatchlistService::class)->visitorNeedsApprovalQueue($visitor));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user