*/ use Billable, HasFactory, HasFeatures, HasUuids, Notifiable, SoftDeletes, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. * * @var list */ protected $fillable = [ 'name', 'email', 'password', 'encryption_salt', 'onboarded_at', 'paywall_seen_at', 'currency_code', 'locale', 'timezone', ]; /** * The attributes that should be hidden for serialization. * * @var list */ protected $hidden = [ 'password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token', ]; /** * Get the attributes that should be cast. * * @return array */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', 'two_factor_confirmed_at' => 'datetime', 'onboarded_at' => 'datetime', 'paywall_seen_at' => 'datetime', 'last_logged_in_at' => 'datetime', 'last_active_at' => 'datetime', ]; } public function isOnboarded(): bool { return $this->onboarded_at !== null; } public function hasSeenPaywall(): bool { return $this->paywall_seen_at !== null; } /** @return HasOne */ public function setting(): HasOne { return $this->hasOne(UserSetting::class); } /** @return HasOne */ public function encryptedMessage(): HasOne { return $this->hasOne(EncryptedMessage::class); } /** @return HasMany */ public function transactions(): HasMany { return $this->hasMany(Transaction::class); } /** @return HasMany */ public function accounts(): HasMany { return $this->hasMany(Account::class); } /** @return HasMany */ public function categories(): HasMany { return $this->hasMany(Category::class); } /** @return HasMany */ public function banks(): HasMany { return $this->hasMany(Bank::class) ->where(function (Builder $query) { $query->whereNull('user_id') ->orWhere('banks.user_id', $this->id); }); } /** @return HasMany */ public function automationRules(): HasMany { return $this->hasMany(AutomationRule::class); } /** @return HasMany */ public function aiConsents(): HasMany { return $this->hasMany(AiConsent::class); } /** @return HasMany */ public function suggestionRuns(): HasMany { return $this->hasMany(SuggestionRun::class); } /** * Whether the user has an active, current-version AI consent. */ public function hasActiveAiConsent(string $scope = AiConsent::SCOPE_FINANCE): bool { return $this->aiConsents()->active($scope)->exists(); } /** * Record an AI consent for the current consent version (idempotent). */ public function recordAiConsent(string $scope = AiConsent::SCOPE_FINANCE): AiConsent { return $this->aiConsents()->firstOrCreate( [ 'scope' => $scope, 'version' => (string) config('ai_suggestions.consent_version'), 'revoked_at' => null, ], ['accepted_at' => now()], ); } /** * Revoke any active AI consents for the given scope. */ public function revokeAiConsent(string $scope = AiConsent::SCOPE_FINANCE): void { $this->aiConsents()->active($scope)->update(['revoked_at' => now()]); } /** @return HasMany */ public function labels(): HasMany { return $this->hasMany(Label::class); } /** @return HasMany */ public function mailLogs(): HasMany { return $this->hasMany(UserMailLog::class); } /** @return HasMany */ public function budgets(): HasMany { return $this->hasMany(Budget::class); } /** @return HasMany */ public function bankingConnections(): HasMany { return $this->hasMany(BankingConnection::class); } public function hasReceivedEmail(DripEmailType $type): bool { return $this->mailLogs()->where('email_type', $type)->exists(); } public function hasProPlan(): bool { if (! config('subscriptions.enabled')) { return true; } return $this->subscribed('default'); } /** * Whether the user can access the given feature on their current plan. */ public function canUseFeature(PlanFeature $feature): bool { if (! $feature->requiresProPlan()) { return true; } return $this->hasProPlan(); } public function hasPastDueSubscription(): bool { if (! config('subscriptions.enabled')) { return false; } $subscription = $this->subscription('default'); return $subscription !== null && $subscription->stripe_status === 'past_due' && ! $subscription->ended(); } public function hasCanceledSubscription(): bool { if (! config('subscriptions.enabled')) { return false; } $subscription = $this->subscription('default'); return $subscription !== null && $subscription->stripe_status === 'canceled' && $subscription->ended(); } /** * The tax rates that should apply to the customer's subscriptions. * * @return array */ public function taxRates(): array { return config('subscriptions.tax_rates', []); } public function isDemoAccount(): bool { return $this->email === config('app.demo.email'); } public function preferredLocale(): string { return $this->locale ?? 'en'; } public function isDeleted(): bool { return $this->trashed(); } public function markAsDeleted(): void { if ($this->trashed()) { return; } DB::transaction(function () { $this->forceFill([ 'email' => $this->deletedEmail(), ])->saveQuietly(); $this->delete(); }); } public function canReceiveEmails(): bool { return ! $this->isDeleted(); } public function wantsBankTransactionsSyncedEmail(): bool { return $this->setting->notify_on_bank_transactions_synced ?? true; } public function routeNotificationForMail(?Notification $notification = null): ?string { if (! $this->canReceiveEmails()) { return null; } $email = trim((string) $this->email); if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) { Log::warning('Skipping mail notification: invalid recipient email', [ 'user_id' => $this->getKey(), 'notification' => $notification ? $notification::class : null, ]); return null; } return $email; } public function sendEmailVerificationNotification(): void { if (! $this->canReceiveEmails()) { return; } $this->notify(new VerifyEmailNotification); } private function deletedEmail(): string { $timestamp = $this->freshTimestamp()->format('YmdHis'); $originalEmail = $this->getOriginal('email'); $candidate = "{$timestamp}_{$originalEmail}"; if (! static::withTrashed()->where('email', $candidate)->exists()) { return $candidate; } return "{$timestamp}_{$this->getKey()}_{$originalEmail}"; } }