diff --git a/app/Modules/Clients/Http/Controllers/Api/ClientsController.php b/app/Modules/Clients/Http/Controllers/Api/ClientsController.php index 2eb06003..e2a5df85 100644 --- a/app/Modules/Clients/Http/Controllers/Api/ClientsController.php +++ b/app/Modules/Clients/Http/Controllers/Api/ClientsController.php @@ -191,7 +191,11 @@ class ClientsController extends Controller $client->storage_quota_mb = $validated['storage_quota_mb'] ?? 0; } + // Approval, and so the moment the seat is spent — same rule the + // web edit screen and approve() answer to. Inside the branch, so a + // capped installation can still edit a client it already holds. if (($validated['active'] ?? false) && $client->account_requested) { + $this->seats->guardClient('active'); $client->account_requested = false; } diff --git a/app/Modules/Clients/Http/Controllers/ClientsController.php b/app/Modules/Clients/Http/Controllers/ClientsController.php index a7abe62b..d25ae216 100644 --- a/app/Modules/Clients/Http/Controllers/ClientsController.php +++ b/app/Modules/Clients/Http/Controllers/ClientsController.php @@ -234,8 +234,13 @@ class ClientsController extends Controller ]); // Activating a pending account through the edit screen counts as - // approval and clears the request flag. + // approval and clears the request flag — which is the moment a + // seat is spent, so the cap is asked here for the same reason + // AccountRequestsController::approve() asks it one screen over. + // Inside the branch, not above it: an installation at its cap must + // still be able to rename a client it already has. if ($client->account_requested && $validated['active']) { + $this->seats->guardClient('active'); $client->account_requested = false; } diff --git a/tests/Feature/Platform/SeatAllowanceTest.php b/tests/Feature/Platform/SeatAllowanceTest.php index 80055756..ee730d48 100644 --- a/tests/Feature/Platform/SeatAllowanceTest.php +++ b/tests/Feature/Platform/SeatAllowanceTest.php @@ -9,6 +9,7 @@ use App\Modules\Platform\Seats\SeatAllowance; use App\Modules\Platform\Settings\Setting; use App\Modules\Platform\Settings\Settings; use Illuminate\Validation\ValidationException; +use Laravel\Sanctum\Sanctum; /** * A cap is only a cap if every door asks. @@ -191,6 +192,53 @@ test('door: approving an account request', function () { expect($pending->refresh()->account_requested)->toBeTrue(); }); +test('door: approving a pending client through the edit screen', function () { + seatLimits(clients: 0); + + $pending = User::factory()->client()->create(['account_requested' => true, 'active' => false]); + + $this->actingAs($this->admin)->patch("/clients/{$pending->id}", [ + 'name' => $pending->name, + 'email' => $pending->email, + 'active' => true, + ])->assertSessionHasErrors('active'); + + // Nothing is written: the guard throws before save(), so a refused + // approval does not leave the name or the flag half-applied. + expect($pending->refresh()->account_requested)->toBeTrue() + ->and($pending->active)->toBeFalse(); +}); + +test('door: approving a pending client through the API', function () { + seatLimits(clients: 0); + + $pending = User::factory()->client()->create(['account_requested' => true, 'active' => false]); + + Sanctum::actingAs($this->admin, ['*']); + + $this->patchJson("/api/v1/clients/{$pending->id}", ['active' => true]) + ->assertStatus(422) + ->assertJsonValidationErrors('active'); + + expect($pending->refresh()->account_requested)->toBeTrue(); +}); + +test('the cap does not block editing a client the installation already holds', function () { + // The guard sits inside the approval branch. Above it, an installation + // sitting at its cap could not rename anybody. + seatLimits(clients: 0); + + $client = User::factory()->client()->create(['account_requested' => false, 'active' => true]); + + $this->actingAs($this->admin)->patch("/clients/{$client->id}", [ + 'name' => 'Renamed Ltd', + 'email' => $client->email, + 'active' => true, + ])->assertSessionHasNoErrors(); + + expect($client->refresh()->name)->toBe('Renamed Ltd'); +}); + test('door: demoting a staff account to client', function () { seatLimits(clients: 0);