feat: complete Epic 1 — team management & permission system

- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration
- Story 1.2: Team page with member list, invitation system with queued email
- Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging
- Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal)
- Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping
- Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching
- 83 new/modified files, 182 tests passing with zero regressions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 00:12:50 +00:00
parent 5dffd2d063
commit c89d1879bf
83 changed files with 5850 additions and 314 deletions

View File

@@ -0,0 +1,245 @@
<?php
use App\Enums\WorkspaceUserRole;
use App\Models\Client;
use App\Models\Declaration;
use App\Models\User;
use App\Models\Workspace;
use Inertia\Testing\AssertableInertia as Assert;
function setupClientTestUser(string $role, array $permissions = []): array
{
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, [
'role' => $role,
'permissions' => $permissions,
]);
session(['current_workspace_id' => $workspace->id]);
return [$user, $workspace];
}
// AC #1: Workers see only clients with assigned declarations
test('worker sees only clients with assigned declarations in index', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$clientWithAssignment = Client::factory()->create(['workspace_id' => $workspace->id]);
$clientWithoutAssignment = Client::factory()->create(['workspace_id' => $workspace->id]);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $clientWithAssignment->id,
'assigned_to' => $worker->id,
]);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $clientWithoutAssignment->id,
'assigned_to' => null,
]);
$response = $this->actingAs($worker)->get(route('clients.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('clients/Index')
->has('clients.data', 1)
->where('clients.data.0.id', $clientWithAssignment->id)
);
});
// AC #3: Owners see all workspace clients
test('owner sees all workspace clients in index', function () {
[$owner, $workspace] = setupClientTestUser(WorkspaceUserRole::Owner);
Client::factory()->count(3)->create(['workspace_id' => $workspace->id]);
$response = $this->actingAs($owner)->get(route('clients.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('clients/Index')
->has('clients.data', 3)
);
});
// AC #3: Managers see all workspace clients
test('manager sees all workspace clients in index', function () {
[$manager, $workspace] = setupClientTestUser(WorkspaceUserRole::Manager);
Client::factory()->count(3)->create(['workspace_id' => $workspace->id]);
$response = $this->actingAs($manager)->get(route('clients.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('clients/Index')
->has('clients.data', 3)
);
});
// AC #5: Worker gets 404 accessing client with no assigned declarations
test('worker gets 404 accessing client with no assigned declarations', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
// No declarations assigned to this worker for this client
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => null,
]);
$response = $this->actingAs($worker)->get(route('clients.show', $client));
$response->assertNotFound();
});
// Worker can access client show when they have assigned declarations
test('worker can access client show when they have assigned declarations', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
Declaration::factory()->create([
'workspace_id' => $workspace->id,
'client_id' => $client->id,
'assigned_to' => $worker->id,
]);
$response = $this->actingAs($worker)->get(route('clients.show', $client));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('clients/Show')
);
});
// AC #6: Worker gets 404 on create
test('worker gets 404 on client create', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$response = $this->actingAs($worker)->get(route('clients.create'));
$response->assertNotFound();
});
// AC #6: Worker gets 404 on store
test('worker gets 404 on client store', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$response = $this->actingAs($worker)->post(route('clients.store'), [
'company_name' => 'Test',
'legal_form' => 'sarl',
'contacts' => [['full_name' => 'Test', 'is_principal' => true]],
]);
$response->assertNotFound();
});
// AC #6: Worker gets 404 on edit
test('worker gets 404 on client edit', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
$response = $this->actingAs($worker)->get(route('clients.edit', $client));
$response->assertNotFound();
});
// AC #6: Worker gets 404 on update
test('worker gets 404 on client update', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
$response = $this->actingAs($worker)->put(route('clients.update', $client), [
'company_name' => 'Updated',
'legal_form' => 'sarl',
'contacts' => [
['full_name' => 'Test Contact', 'is_principal' => true],
],
]);
$response->assertNotFound();
});
// AC #6: Worker gets 404 on destroy
test('worker gets 404 on client destroy', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
$response = $this->actingAs($worker)->delete(route('clients.destroy', $client));
$response->assertNotFound();
});
// AC #3: Manager can access all CRUD operations
test('manager can access all client CRUD operations', function () {
[$manager, $workspace] = setupClientTestUser(WorkspaceUserRole::Manager);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
// Index
$this->actingAs($manager)->get(route('clients.index'))->assertOk();
// Show
$this->actingAs($manager)->get(route('clients.show', $client))->assertOk();
// Create page
$this->actingAs($manager)->get(route('clients.create'))->assertOk();
// Edit page
$this->actingAs($manager)->get(route('clients.edit', $client))->assertOk();
});
// AC #3: Owner can access all CRUD operations
test('owner can access all client CRUD operations', function () {
[$owner, $workspace] = setupClientTestUser(WorkspaceUserRole::Owner);
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
// Index
$this->actingAs($owner)->get(route('clients.index'))->assertOk();
// Show
$this->actingAs($owner)->get(route('clients.show', $client))->assertOk();
// Create page
$this->actingAs($owner)->get(route('clients.create'))->assertOk();
// Edit page
$this->actingAs($owner)->get(route('clients.edit', $client))->assertOk();
});
// AC #10: canCreate/canEdit/canDelete props are false for Workers
test('worker gets canCreate canEdit canDelete as false in index', function () {
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
$response = $this->actingAs($worker)->get(route('clients.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('canCreate', false)
->where('canEdit', false)
->where('canDelete', false)
);
});
// AC #10: canCreate/canEdit/canDelete props are true for Owners
test('owner gets canCreate canEdit canDelete as true in index', function () {
[$owner, $workspace] = setupClientTestUser(WorkspaceUserRole::Owner);
$response = $this->actingAs($owner)->get(route('clients.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('canCreate', true)
->where('canEdit', true)
->where('canDelete', true)
);
});