*/ use Billable, HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. * * @var list */ protected $fillable = [ 'name', 'email', 'portal', 'registration_type', 'language', 'is_active', 'is_super_admin', 'last_login_at', 'last_login_ip', 'gdpr_consent_at', 'last_seen_at', 'legacy_portal', 'legacy_id', 'password', 'press_release_quota_used_this_month', ]; /** * The attributes that should be hidden for serialization. * * @var list */ protected $hidden = [ 'password', 'remember_token', ]; /** * Get the attributes that should be cast. * * @return array */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'portal' => Portal::class, 'registration_type' => RegistrationType::class, 'is_active' => 'boolean', 'is_super_admin' => 'boolean', 'last_login_at' => 'datetime', 'gdpr_consent_at' => 'datetime', 'last_seen_at' => 'datetime', 'deleted_at' => 'datetime', 'password' => 'hashed', 'press_release_quota_used_this_month' => 'integer', ]; } /** * Adresse für die Stripe-Customer-Anlage (Cashier-Hook). Stripe Tax * braucht eine gültige Kundenadresse — falls lokal eine * Rechnungsadresse gepflegt ist, wird sie direkt mitgegeben; sonst * speichert der Checkout die dort erfasste Adresse (customer_update). * * @return array|null */ public function stripeAddress(): ?array { $address = $this->billingAddress; if (! $address) { return null; } return [ 'line1' => $address->address1, 'line2' => $address->address2, 'postal_code' => $address->postal_code, 'city' => $address->city, 'country' => $address->country_code, ]; } /** * Buchungs-Voraussetzung (Entscheidung 12.06.2026): Tarif- und * Einzel-PM-Checkouts erfordern eine vollständige Rechnungsadresse. */ public function hasCompleteBillingAddress(): bool { return (bool) $this->billingAddress?->isComplete(); } /** * Der Tarif des aktiven Stripe-Abos, aufgelöst über die in `plans` * gepflegten Stripe-Preis-IDs. Null ohne (gültiges) Abo. */ public function currentPlan(): ?Plan { $subscription = $this->subscription(); if (! $subscription?->valid()) { return null; } $priceId = $subscription->stripe_price; if (! $priceId) { return null; } return Plan::query() ->where('stripe_price_id_monthly', $priceId) ->orWhere('stripe_price_id_yearly', $priceId) ->first(); } /** * Hat dieser User ein unbegrenztes PM-Kontingent? * * Entscheidung 12.06.2026: Bestandskunden (aktive/grandfathered * Legacy-Vereinbarung) behalten ihren Bestandsschutz unverändert — * das Alt-Produkt sah unbegrenzte PMs vor. Solange der Launch-Schalter * `billing.enforce_booking` aus ist, gilt das Kontingent für niemanden. */ public function hasUnlimitedPressReleaseQuota(): bool { if (! config('billing.enforce_booking')) { return true; } return $this->userPaymentOptions() ->whereIn('status', [ UserPaymentOptionStatus::Active->value, UserPaymentOptionStatus::Grandfathered->value, ]) ->exists(); } /** * Verbleibendes PM-Kontingent: Rest des Plan-Monatskontingents plus * bezahlte, noch nicht eingelöste Einzel-/Extra-PM-Käufe. * Null bedeutet unbegrenzt. */ public function pressReleaseQuotaRemaining(): ?int { if ($this->hasUnlimitedPressReleaseQuota()) { return null; } $planRemaining = max( 0, ($this->currentPlan()?->press_release_quota ?? 0) - (int) $this->press_release_quota_used_this_month, ); return $planRemaining + $this->singlePurchases()->grantingSubmission()->count(); } /** * Gesamtes PM-Kontingent (Plan-Monatskontingent plus offene Einmalkäufe) * für die Anzeige „verbleibend / gesamt". Null bedeutet unbegrenzt. */ public function pressReleaseQuotaTotal(): ?int { if ($this->hasUnlimitedPressReleaseQuota()) { return null; } return ($this->currentPlan()?->press_release_quota ?? 0) + $this->singlePurchases()->grantingSubmission()->count(); } /** * Submit-Gate aus dem Decision-Update §5.1: Einreichen zur Prüfung * erfordert eine aktive Buchung. * * Hybrides Modell: Eine Buchung ist entweder ein aktives Stripe-Abo * (Cashier, STR-Kreis), ein bezahlter Einmalkauf (Einzel-PM/Extra-PM) * oder eine laufende Legacy-Zahlungsvereinbarung (manueller MAN-Kreis). * Solange `billing.enforce_booking` deaktiviert ist, bleibt das Gate * offen (Launch-Schalter). */ public function hasActiveBooking(): bool { if (! config('billing.enforce_booking')) { return true; } if ($this->subscribed()) { return true; } if ($this->singlePurchases()->grantingSubmission()->exists()) { return true; } return $this->userPaymentOptions() ->whereIn('status', [ UserPaymentOptionStatus::Active->value, UserPaymentOptionStatus::Grandfathered->value, ]) ->exists(); } /** * Get the user's initials */ public function initials(): string { return Str::of($this->name) ->explode(' ') ->map(fn (string $name) => Str::of($name)->substr(0, 1)) ->implode(''); } public function profile(): HasOne { return $this->hasOne(Profile::class); } public function magicLinks(): HasMany { return $this->hasMany(MagicLink::class); } public function ownedCompanies(): HasMany { return $this->hasMany(Company::class, 'owner_user_id'); } public function companies(): BelongsToMany { return $this->belongsToMany(Company::class) ->withPivot('role') ->withTimestamps(); } public function contacts(): BelongsToMany { return $this->belongsToMany(Contact::class) ->withTimestamps(); } public function pressReleases(): HasMany { return $this->hasMany(PressRelease::class); } public function newsletterSubscriptions(): HasMany { return $this->hasMany(NewsletterSubscription::class); } public function billingAddress(): HasOne { return $this->hasOne(BillingAddress::class); } public function userPaymentOptions(): HasMany { return $this->hasMany(UserPaymentOption::class); } public function singlePurchases(): HasMany { return $this->hasMany(SinglePurchase::class); } /** * Lokale Rechnungen (STR- und MAN-Kreis). Überschreibt bewusst die * gleichnamige Cashier-Methode — Stripe-Rechnungen werden beim * Webhook-Sync (9E) in diese Tabelle gespiegelt. */ public function invoices(): HasMany { return $this->hasMany(Invoice::class); } public function legacyInvoices(): HasMany { return $this->hasMany(LegacyInvoice::class); } public function filterPresets(): HasMany { return $this->hasMany(UserFilterPreset::class); } public function canAccessAdmin(): bool { if (! $this->is_active) { return false; } if ($this->is_super_admin) { return true; } return $this->hasAnyRole(['admin', 'editor']); } public function canAccessCustomer(): bool { if (! $this->is_active) { return false; } return $this->hasAnyRole(['admin', 'editor', 'customer']); } }