customer(); if ($customer !== null && $customer->seats()->count() === 0) { try { $customer->seats()->firstOrCreate( ['email' => $customer->email], ['name' => $customer->name, 'role' => 'owner', 'status' => 'active', 'invited_at' => now()], ); } catch (UniqueConstraintViolationException) { // Another concurrent first visit created it — fine. } } } public function invite(): void { $customer = $this->customer(); if ($customer === null) { return; } $data = $this->validate(); // Serialize the limit check with the insert so two concurrent invites for // the last free seat can't both pass (lock the customer row). $result = DB::transaction(function () use ($customer, $data) { $locked = Customer::query()->whereKey($customer->id)->lockForUpdate()->first(); if ($locked->seats()->where('email', $data['inviteEmail'])->exists()) { return 'duplicate'; } if ($this->usedSeats($locked) >= $this->seatLimit($locked)) { return 'limit'; } $locked->seats()->create([ 'email' => $data['inviteEmail'], 'name' => $data['inviteName'] ?: null, 'role' => $data['inviteRole'], 'status' => 'invited', 'invited_at' => now(), ]); return 'ok'; }); if ($result === 'limit') { $this->addError('inviteEmail', __('users.limit_reached')); return; } if ($result === 'duplicate') { $this->addError('inviteEmail', __('users.duplicate')); return; } $this->reset('inviteEmail', 'inviteName', 'inviteRole'); $this->inviteRole = 'member'; $this->dispatch('notify', message: __('users.invited')); } public function setRole(string $uuid, string $role): void { if (! in_array($role, Seat::ROLES, true)) { return; } $seat = $this->seat($uuid); if ($seat === null) { return; } // Never leave the account without an owner. if ($seat->role === 'owner' && $role !== 'owner' && $this->ownerCount() <= 1) { $this->dispatch('notify', message: __('users.last_owner')); return; } $seat->update(['role' => $role]); } public function revoke(string $uuid): void { $seat = $this->seat($uuid); if ($seat === null) { return; } if ($seat->role === 'owner' && $this->ownerCount() <= 1) { $this->dispatch('notify', message: __('users.last_owner')); return; } $seat->delete(); $this->dispatch('notify', message: __('users.revoked')); } public function resend(string $uuid): void { // Invite delivery is mocked for now. if ($this->seat($uuid) !== null) { $this->dispatch('notify', message: __('users.resent')); } } private function ownerCount(): int { $customer = $this->customer(); return $customer ? $customer->seats()->where('role', 'owner')->count() : 0; } private function usedSeats(Customer $customer): int { return $customer->seats()->where('status', '!=', 'revoked')->count(); } private function seatLimit(Customer $customer): int { // The entitlement follows the active (or cancelling) package, not a newer // failed/deprovisioned record. $instance = $customer->instances()->whereIn('status', ['active', 'cancellation_scheduled'])->latest('id')->first() ?? $customer->instances()->latest('id')->first(); $plan = $instance->plan ?? 'start'; return (int) config("provisioning.plans.$plan.seats", 5); } private function seat(string $uuid): ?Seat { $customer = $this->customer(); return $customer?->seats()->where('uuid', $uuid)->first(); } private function customer(): ?Customer { $user = auth()->user(); if (! $user) { return null; } return Customer::query()->where('user_id', $user->id)->first() ?? Customer::query()->where('email', $user->email)->first(); } public function render() { $customer = $this->customer(); $seats = $customer ? $customer->seats()->orderByRaw("role = 'owner' desc")->orderBy('email')->get() : collect(); return view('livewire.users', [ 'seats' => $seats, 'used' => $customer ? $this->usedSeats($customer) : 0, 'limit' => $customer ? $this->seatLimit($customer) : 0, 'roles' => Seat::ROLES, ]); } }