diff --git a/app/Http/Controllers/AirlineController.php b/app/Http/Controllers/AirlineController.php
index a9fe113..451d83c 100644
--- a/app/Http/Controllers/AirlineController.php
+++ b/app/Http/Controllers/AirlineController.php
@@ -57,6 +57,7 @@ public function foundStore(Request $request)
'unit_is_lbs' => $request->boolean('unit_is_lbs'),
'active' => true,
'require_pirep_review' => true,
+ 'owner_user_id' => auth()->id(),
]);
$airline->users()->attach(auth()->id(), ['role' => 'Manager']);
diff --git a/app/Http/Controllers/MemberController.php b/app/Http/Controllers/MemberController.php
new file mode 100644
index 0000000..b303713
--- /dev/null
+++ b/app/Http/Controllers/MemberController.php
@@ -0,0 +1,141 @@
+middleware('auth');
+ }
+
+ public function index()
+ {
+ $airline = session('activeairline');
+
+ abort_unless($airline && auth()->user()->isManagerOf($airline), 403);
+
+ // Reload from DB — the session model is a snapshot
+ $airline = Airline::findOrFail($airline->id);
+
+ // Owner first (represented by -1), then Managers (0), Dispatchers (1), Pilots (2); alphabetical within each.
+ // Sorted in PHP so it stays portable across MySQL/SQLite (tests).
+ $roleOrder = ['Manager' => 0, 'Dispatcher' => 1, 'Pilot' => 2];
+ $members = $airline->users()
+ ->orderBy('name')
+ ->get()
+ ->sortBy(fn ($member) => [
+ $member->id === $airline->owner_user_id ? -1 : ($roleOrder[$member->pivot->role] ?? 99),
+ $member->name
+ ])
+ ->values();
+
+ return view('manager.members', compact('airline', 'members'));
+ }
+
+ public function update(Request $request, User $member)
+ {
+ $airline = session('activeairline');
+
+ abort_unless($airline && auth()->user()->isManagerOf($airline), 403);
+ abort_unless($airline->isMember($member), 403);
+
+ // Reload fresh to have accurate owner_user_id
+ $airline = Airline::findOrFail($airline->id);
+
+ $viewerIsOwner = auth()->user()->isOwnerOf($airline);
+ $targetIsOwner = $airline->owner_user_id === $member->id;
+
+ abort_if($targetIsOwner, 403);
+
+ $request->validate(['role' => 'required|in:Pilot,Dispatcher,Manager']);
+
+ if (! $viewerIsOwner) {
+ $isTargetManager = $member->hasAirlineRole($airline, 'Manager');
+ abort_if($isTargetManager || $request->role === 'Manager', 403);
+ }
+
+ $airline->users()->updateExistingPivot($member->id, ['role' => $request->role]);
+
+ activity()
+ ->causedBy(auth()->user())
+ ->performedOn($airline)
+ ->withProperties(['level' => ActivityLevel::INFO])
+ ->event('airline_member_role_changed')
+ ->log($member->name . ' is now ' . $request->role . ' at ' . $airline->name);
+
+ return back()->with('success', $member->name . "'s role updated to " . $request->role . '.');
+ }
+
+ public function destroy(User $member)
+ {
+ $airline = session('activeairline');
+
+ abort_unless($airline && auth()->user()->isManagerOf($airline), 403);
+ abort_unless($airline->isMember($member), 403);
+
+ // Reload fresh to have accurate owner_user_id
+ $airline = Airline::findOrFail($airline->id);
+
+ $viewerIsOwner = auth()->user()->isOwnerOf($airline);
+ $targetIsOwner = $airline->owner_user_id === $member->id;
+
+ abort_if($targetIsOwner, 403);
+
+ if (! $viewerIsOwner) {
+ $isTargetManager = $member->hasAirlineRole($airline, 'Manager');
+ abort_if($isTargetManager, 403);
+ }
+
+ $airline->users()->detach($member->id);
+
+ activity()
+ ->causedBy(auth()->user())
+ ->performedOn($airline)
+ ->withProperties(['level' => ActivityLevel::INFO])
+ ->event('airline_member_removed')
+ ->log($member->name . ' was removed from ' . $airline->name);
+
+ return back()->with('success', $member->name . ' has been removed from the airline.');
+ }
+
+ public function transferOwnership(User $member)
+ {
+ $airline = session('activeairline');
+
+ abort_unless($airline, 403);
+ $airline = Airline::findOrFail($airline->id);
+
+ abort_unless(auth()->user()->isOwnerOf($airline), 403);
+ abort_unless($airline->isMember($member), 403);
+ abort_if($member->id === auth()->id(), 422);
+
+ DB::transaction(function () use ($airline, $member) {
+ // Promote new owner to Manager role in memberships
+ $airline->users()->updateExistingPivot($member->id, ['role' => 'Manager']);
+ // Keep old owner as Manager in memberships
+ $airline->users()->updateExistingPivot(auth()->id(), ['role' => 'Manager']);
+
+ $airline->owner_user_id = $member->id;
+ $airline->save();
+ });
+
+ // re-put the fresh airline into the session
+ session(['activeairline' => $airline]);
+
+ activity()
+ ->causedBy(auth()->user())
+ ->performedOn($airline)
+ ->withProperties(['level' => ActivityLevel::INFO])
+ ->event('airline_ownership_transferred')
+ ->log('Ownership of ' . $airline->name . ' transferred to ' . $member->name);
+
+ return back()->with('success', 'Ownership transferred to ' . $member->name . '. You are now a Manager.');
+ }
+}
diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php
index ef25e4c..df81ad7 100644
--- a/app/Http/Controllers/SetupController.php
+++ b/app/Http/Controllers/SetupController.php
@@ -121,6 +121,8 @@ public function store(Request $request)
$user->assignRole($superAdminRole);
$airline->users()->attach($user, ['role' => 'Manager']);
+ $airline->owner_user_id = $user->id;
+ $airline->save();
auth()->login($user);
session(['activeairline' => $airline]);
diff --git a/app/Models/Airline.php b/app/Models/Airline.php
index 2ac37c6..47caf01 100644
--- a/app/Models/Airline.php
+++ b/app/Models/Airline.php
@@ -27,6 +27,7 @@ class Airline extends Model
'active',
'require_pirep_review',
'location_continuity',
+ 'owner_user_id',
];
protected $casts = [
@@ -83,4 +84,14 @@ public function notams(): HasMany
return $this->hasMany(Notam::class);
}
+ public function owner(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+ {
+ return $this->belongsTo(User::class, 'owner_user_id');
+ }
+
+ public function isOwnedBy(User $u): bool
+ {
+ return $this->owner_user_id !== null && $this->owner_user_id === $u->id;
+ }
+
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 222462c..273943b 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -103,14 +103,19 @@ public function hasAirlineRole(Airline $airline, string|array $roles): bool
->exists();
}
+ public function isOwnerOf(Airline $airline): bool
+ {
+ return $airline->owner_user_id === $this->id;
+ }
+
public function isManagerOf(Airline $airline): bool
{
- return $this->hasAirlineRole($airline, 'Manager');
+ return $this->isOwnerOf($airline) || $this->hasAirlineRole($airline, 'Manager');
}
public function canReviewFlightsFor(Airline $airline): bool
{
- return $this->hasAirlineRole($airline, ['Dispatcher', 'Manager']);
+ return $this->isOwnerOf($airline) || $this->hasAirlineRole($airline, ['Dispatcher', 'Manager']);
}
public function countNewNotifications() {
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index d6b3856..3ae1a62 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -148,7 +148,21 @@ public function boot(): void
});
Gate::before(function ($user, $ability) {
- return $user->hasRole('Super-Admin') ? true : null;
+ $activeAirline = session('activeairline');
+ if ($activeAirline) {
+ if ($ability === 'add aircraft' || $ability === 'edit aircraft') {
+ return $user->isManagerOf($activeAirline);
+ }
+ if ($ability === 'review flight') {
+ return $user->canReviewFlightsFor($activeAirline);
+ }
+ }
+
+ if ($user->hasRole('Super-Admin')) {
+ return true;
+ }
+
+ return null;
});
}
}
\ No newline at end of file
diff --git a/database/migrations/2026_07_23_114200_add_owner_to_airlines_table.php b/database/migrations/2026_07_23_114200_add_owner_to_airlines_table.php
new file mode 100644
index 0000000..6991341
--- /dev/null
+++ b/database/migrations/2026_07_23_114200_add_owner_to_airlines_table.php
@@ -0,0 +1,48 @@
+foreignId('owner_user_id')->nullable()->after('id')->constrained('users')->nullOnDelete();
+ });
+
+ // Backfill
+ $airlines = DB::table('airlines')->get();
+ foreach ($airlines as $airline) {
+ $ownerUserId = DB::table('airline_memberships')
+ ->where('airline_id', $airline->id)
+ ->where('role', 'Manager')
+ ->orderBy('created_at')
+ ->orderBy('id')
+ ->value('user_id');
+
+ if (!$ownerUserId) {
+ $ownerUserId = DB::table('airline_memberships')
+ ->where('airline_id', $airline->id)
+ ->orderBy('created_at')
+ ->orderBy('id')
+ ->value('user_id');
+ }
+
+ if ($ownerUserId) {
+ DB::table('airlines')
+ ->where('id', $airline->id)
+ ->update(['owner_user_id' => $ownerUserId]);
+ }
+ }
+ }
+
+ public function down(): void
+ {
+ Schema::table('airlines', function (Blueprint $table) {
+ $table->dropConstrainedForeignId('owner_user_id');
+ });
+ }
+};
diff --git a/database/seeders/AirlineMembershipSeeder.php b/database/seeders/AirlineMembershipSeeder.php
index b7ad9dd..ba0448e 100644
--- a/database/seeders/AirlineMembershipSeeder.php
+++ b/database/seeders/AirlineMembershipSeeder.php
@@ -44,5 +44,30 @@ public function run(): void
}
}
}
+
+ // Set owners for all airlines
+ $airlines = DB::table('airlines')->get();
+ foreach ($airlines as $airline) {
+ $ownerUserId = DB::table('airline_memberships')
+ ->where('airline_id', $airline->id)
+ ->where('role', 'Manager')
+ ->orderBy('created_at')
+ ->orderBy('id')
+ ->value('user_id');
+
+ if (!$ownerUserId) {
+ $ownerUserId = DB::table('airline_memberships')
+ ->where('airline_id', $airline->id)
+ ->orderBy('created_at')
+ ->orderBy('id')
+ ->value('user_id');
+ }
+
+ if ($ownerUserId) {
+ DB::table('airlines')
+ ->where('id', $airline->id)
+ ->update(['owner_user_id' => $ownerUserId]);
+ }
+ }
}
}
diff --git a/resources/views/layouts/_navbar.blade.php b/resources/views/layouts/_navbar.blade.php
index d4e7cfd..d3e20cb 100644
--- a/resources/views/layouts/_navbar.blade.php
+++ b/resources/views/layouts/_navbar.blade.php
@@ -58,7 +58,7 @@
@endphp
@if($canReviewFlights || $isAirlineManager)
-
+
Management
@@ -70,6 +70,7 @@
@endif
@if($isAirlineManager)
- Operations
+ - Members
- Invite codes
- Announcements
@endif
diff --git a/resources/views/manager/_sidebar.blade.php b/resources/views/manager/_sidebar.blade.php
index 97b499a..927729b 100644
--- a/resources/views/manager/_sidebar.blade.php
+++ b/resources/views/manager/_sidebar.blade.php
@@ -1,5 +1,5 @@
{{-- Airline management sections sidebar. Pass $active
- ('operations' | 'invitecodes' | 'announcements') to highlight the current section. --}}
+ ('operations' | 'members' | 'invitecodes' | 'announcements') to highlight the current section. --}}
@php($active = $active ?? 'operations')
@@ -8,6 +8,10 @@
class="list-group-item list-group-item-action {{ $active === 'operations' ? 'active' : '' }}">
Operations
+
+ Members
+
Invite codes
diff --git a/resources/views/manager/members.blade.php b/resources/views/manager/members.blade.php
new file mode 100644
index 0000000..f033940
--- /dev/null
+++ b/resources/views/manager/members.blade.php
@@ -0,0 +1,130 @@
+@extends('layouts.app')
+
+@section('title', 'Members')
+
+@section('content')
+
+@php
+ $viewerIsOwner = auth()->user()->isOwnerOf($airline);
+@endphp
+
+
+
+
Airline Management
+
{{ $airline->name }} ({{ $airline->icao_callsign }})
+
+
+
+
+ {{-- Sidebar: airline settings sections --}}
+ @include('manager._sidebar', ['active' => 'members'])
+
+ {{-- Main: members --}}
+
+ @if(session('success'))
+
+ {{ session('success') }}
+
+
+ @endif
+
+ @if($errors->any())
+
+ {{ $errors->first() }}
+
+
+ @endif
+
+
+
+
+
+
+
+ | Name |
+ Role |
+ |
+
+
+
+ @forelse($members as $member)
+ @php
+ $rowIsOwner = $airline->owner_user_id === $member->id;
+ $rowIsManager = $member->pivot->role === 'Manager';
+ @endphp
+
+ |
+ {{ $member->name }}
+ @if($member->id === auth()->id())
+ You
+ @endif
+ @if($rowIsOwner)
+ Owner
+ @endif
+ |
+
+ @if($rowIsOwner)
+ Owner
+ @else
+ @if($rowIsManager && ! $viewerIsOwner)
+ Manager
+ @else
+
+ @endif
+ @endif
+ |
+
+ @if(! $rowIsOwner)
+
+ @if($viewerIsOwner)
+
+ @endif
+
+ @if($viewerIsOwner || ! $rowIsManager)
+
+ @endif
+
+ @endif
+ |
+
+ @empty
+
+ | No members yet. |
+
+ @endforelse
+
+
+
+
+
+
+
+@endsection
diff --git a/routes/web.php b/routes/web.php
index 8f291c7..0b289ca 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -12,6 +12,7 @@
use App\Http\Controllers\HomeController;
use App\Http\Controllers\PortalController;
use App\Http\Controllers\InviteCodeController;
+use App\Http\Controllers\MemberController;
use App\Http\Controllers\NotamController;
use App\Http\Controllers\AirlineController;
use App\Http\Controllers\SettingsController;
@@ -113,6 +114,12 @@
Route::get('/airline/settings', [AirlineController::class, 'settings'])->name('airline.settings');
Route::put('/airline/settings', [AirlineController::class, 'updateSettings'])->name('airline.settings.update');
+ // Member management (manager check enforced in controller)
+ Route::get('/airline/members', [MemberController::class, 'index'])->name('members.index');
+ Route::put('/airline/members/{member}', [MemberController::class, 'update'])->name('members.update');
+ Route::delete('/airline/members/{member}', [MemberController::class, 'destroy'])->name('members.destroy');
+ Route::put('/airline/members/{member}/transfer-ownership', [MemberController::class, 'transferOwnership'])->name('members.transfer');
+
// Announcements / NOTAMs (manager check enforced in controller)
Route::get('/airline/announcements', [NotamController::class, 'index'])->name('notams.index');
Route::post('/airline/announcements', [NotamController::class, 'store'])->name('notams.store');
diff --git a/tests/Concerns/SeedsDomain.php b/tests/Concerns/SeedsDomain.php
index 7d83b25..7f1a971 100644
--- a/tests/Concerns/SeedsDomain.php
+++ b/tests/Concerns/SeedsDomain.php
@@ -68,4 +68,18 @@ protected function memberOf(Airline $airline, string $role = 'Pilot'): User
return $user;
}
+
+ /**
+ * Create a user, attach them to the airline as a Manager, and set them
+ * as the owner of the airline.
+ */
+ protected function ownerOf(Airline $airline): User
+ {
+ $user = User::factory()->create();
+ $user->airlines()->attach($airline->id, ['role' => 'Manager']);
+ $airline->owner_user_id = $user->id;
+ $airline->save();
+
+ return $user;
+ }
}
diff --git a/tests/Feature/MemberManagementTest.php b/tests/Feature/MemberManagementTest.php
new file mode 100644
index 0000000..f136ede
--- /dev/null
+++ b/tests/Feature/MemberManagementTest.php
@@ -0,0 +1,298 @@
+seedReferenceData();
+ }
+
+ private function roleOf($user, Airline $airline): ?string
+ {
+ return $user->fresh()->airlines()->find($airline->id)?->pivot->role;
+ }
+
+ public function test_founding_sets_owner_and_manager_role(): void
+ {
+ $user = User::factory()->create();
+ $user->assignRole('Super-Admin');
+
+ $this->actingAs($user)
+ ->post(route('airline.found.store'), [
+ 'airline_name' => 'Test Airline',
+ 'airline_prefix' => 'TA',
+ 'airline_icao' => 'TAL',
+ 'airline_callsign' => 'TESTAIR',
+ 'airline_hub' => 'EDDF',
+ 'airline_country' => 'DE',
+ 'airline_desc' => 'A test airline',
+ 'airline_website' => 'https://example.com',
+ 'airline_founded' => '2026-07-23',
+ 'unit_is_lbs' => 0,
+ ])
+ ->assertRedirect();
+
+ $airline = Airline::where('icao_callsign', 'TAL')->first();
+ $this->assertNotNull($airline);
+ $this->assertSame($user->id, $airline->owner_user_id);
+ $this->assertTrue($user->isManagerOf($airline));
+ $this->assertSame('Manager', $this->roleOf($user, $airline));
+ }
+
+ public function test_owner_can_promote_pilot_to_manager(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $pilot), ['role' => 'Manager'])
+ ->assertRedirect();
+
+ $this->assertSame('Manager', $this->roleOf($pilot, $airline));
+ }
+
+ public function test_owner_can_demote_manager_to_pilot(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $manager), ['role' => 'Pilot'])
+ ->assertRedirect();
+
+ $this->assertSame('Pilot', $this->roleOf($manager, $airline));
+ }
+
+ public function test_non_owner_manager_can_change_pilot_to_dispatcher(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $pilot), ['role' => 'Dispatcher'])
+ ->assertRedirect();
+
+ $this->assertSame('Dispatcher', $this->roleOf($pilot, $airline));
+ }
+
+ public function test_non_owner_manager_cannot_promote_to_manager(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $pilot), ['role' => 'Manager'])
+ ->assertForbidden();
+
+ $this->assertSame('Pilot', $this->roleOf($pilot, $airline));
+ }
+
+ public function test_non_owner_manager_cannot_demote_manager(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+ $otherManager = $this->memberOf($airline, 'Manager');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $otherManager), ['role' => 'Pilot'])
+ ->assertForbidden();
+
+ $this->assertSame('Manager', $this->roleOf($otherManager, $airline));
+ }
+
+ public function test_non_owner_manager_cannot_remove_manager(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+ $otherManager = $this->memberOf($airline, 'Manager');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->delete(route('members.destroy', $otherManager))
+ ->assertForbidden();
+
+ $this->assertTrue($airline->isMember($otherManager->fresh()));
+ }
+
+ public function test_nobody_can_update_or_remove_owner(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+
+ // Manager trying to update owner
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $owner), ['role' => 'Pilot'])
+ ->assertForbidden();
+
+ // Manager trying to remove owner
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->delete(route('members.destroy', $owner))
+ ->assertForbidden();
+
+ // Owner trying to demote self
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $owner), ['role' => 'Pilot'])
+ ->assertForbidden();
+
+ // Owner trying to remove self
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->delete(route('members.destroy', $owner))
+ ->assertForbidden();
+
+ $this->assertSame('Manager', $this->roleOf($owner, $airline));
+ $this->assertTrue($airline->isMember($owner->fresh()));
+ }
+
+ public function test_owner_can_transfer_ownership(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $member = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.transfer', $member))
+ ->assertRedirect();
+
+ $airline = $airline->fresh();
+ $this->assertSame($member->id, $airline->owner_user_id);
+ $this->assertSame('Manager', $this->roleOf($owner, $airline));
+ $this->assertSame('Manager', $this->roleOf($member, $airline));
+ $this->assertSame($airline->owner_user_id, session('activeairline')->owner_user_id);
+ }
+
+ public function test_non_owner_cannot_transfer_ownership(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.transfer', $pilot))
+ ->assertForbidden();
+
+ $this->assertSame($owner->id, $airline->fresh()->owner_user_id);
+ }
+
+ public function test_owner_cannot_transfer_to_self(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.transfer', $owner))
+ ->assertStatus(422);
+
+ $this->assertSame($owner->id, $airline->fresh()->owner_user_id);
+ }
+
+ public function test_non_manager_cannot_access_members(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($pilot)
+ ->withSession(['activeairline' => $airline])
+ ->get(route('members.index'))
+ ->assertForbidden();
+ }
+
+ public function test_manager_cannot_update_member_of_another_airline(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+
+ $otherAirline = Airline::factory()->create();
+ $outsider = $this->memberOf($otherAirline, 'Pilot');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.update', $outsider), ['role' => 'Manager'])
+ ->assertForbidden();
+ }
+
+ public function test_manager_can_remove_pilot(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $manager = $this->memberOf($airline, 'Manager');
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($manager)
+ ->withSession(['activeairline' => $airline])
+ ->delete(route('members.destroy', $pilot))
+ ->assertRedirect();
+
+ $this->assertFalse($airline->isMember($pilot->fresh()));
+ }
+
+ public function test_ownership_transfer_preserves_add_aircraft_permission_for_both(): void
+ {
+ $airline = Airline::factory()->create();
+ $owner = $this->ownerOf($airline);
+ $member = $this->memberOf($airline, 'Pilot');
+
+ // Transfer ownership
+ $this->actingAs($owner)
+ ->withSession(['activeairline' => $airline])
+ ->put(route('members.transfer', $member))
+ ->assertRedirect();
+
+ // New owner (was Pilot) can add aircraft
+ $this->actingAs($member->fresh())
+ ->withSession(['activeairline' => $airline])
+ ->get(route('createaircraft'))
+ ->assertOk();
+
+ // Old owner (now Manager) can also still add aircraft
+ $this->actingAs($owner->fresh())
+ ->withSession(['activeairline' => $airline])
+ ->get(route('createaircraft'))
+ ->assertOk();
+ }
+
+ public function test_pilot_does_not_have_add_aircraft_permission_on_active_airline(): void
+ {
+ $airline = Airline::factory()->create();
+ $pilot = $this->memberOf($airline, 'Pilot');
+
+ $this->actingAs($pilot)
+ ->withSession(['activeairline' => $airline])
+ ->get(route('createaircraft'))
+ ->assertForbidden();
+ }
+}