*/ use HasFactory, HasUuid; protected $fillable = [ 'user_id', 'name', 'contact_name', 'email', 'phone', 'vat_id', 'billing_address', 'locale', 'stripe_customer_id', 'status', 'closed_at', 'brand_display_name', 'brand_logo_path', 'brand_primary_color', 'brand_accent_color', ]; protected function casts(): array { return ['closed_at' => 'datetime']; } public function seats(): HasMany { return $this->hasMany(Seat::class); } /** * Resolve branding: customer values where set, else CluPilot defaults. Used * for previews and snapshotted into the provisioning run so retries are * deterministic. NULL is stored for "unset" — defaults are never copied in. * * @return array{display_name:string,logo_path:?string,primary_color:string,accent_color:string,is_default:bool} */ public function brandingResolved(): array { $defaults = (array) config('provisioning.branding_defaults'); return [ 'display_name' => $this->brand_display_name ?: ($defaults['display_name'] ?? 'CluPilot'), 'logo_path' => $this->brand_logo_path ?: ($defaults['logo_path'] ?? null), 'primary_color' => $this->brand_primary_color ?: ($defaults['primary_color'] ?? '#f97316'), 'accent_color' => $this->brand_accent_color ?: ($defaults['accent_color'] ?? '#c2560a'), 'is_default' => $this->brand_display_name === null && $this->brand_logo_path === null && $this->brand_primary_color === null && $this->brand_accent_color === null, ]; } public function user(): BelongsTo { return $this->belongsTo(User::class); } public function orders(): HasMany { return $this->hasMany(Order::class); } public function instances(): HasMany { return $this->hasMany(Instance::class); } /** * Find or create the portal login account for this customer and link it. * Race-safe against the unique email index, and never links an operator * account — that would carry admin authorization into an impersonated * "customer" session. */ public function ensureUser(): User { if ($this->user) { return $this->user; } if (($existing = User::query()->where('email', $this->email)->first()) !== null) { $this->assertNotAdmin($existing); $this->update(['user_id' => $existing->id]); return $existing; } try { $user = User::query()->create([ 'email' => $this->email, 'name' => $this->name, 'password' => Hash::make(Str::random(40)), 'is_admin' => false, ]); } catch (UniqueConstraintViolationException) { // Concurrent first-time creation — re-fetch and link the winner. $user = User::query()->where('email', $this->email)->firstOrFail(); $this->assertNotAdmin($user); } $this->update(['user_id' => $user->id]); return $user->refresh(); } private function assertNotAdmin(User $user): void { if ($user->is_admin) { throw new RuntimeException( "Refusing to link admin account {$user->email} as a portal login for customer {$this->id}.", ); } } }