'array', 'settings' => 'array', 'notification_preferences' => 'array', 'last_activity_at' => 'datetime', 'anonymized_at' => 'datetime', 'pending_deletion_at' => 'datetime', 'deletion_warning_sent_at' => 'datetime', 'total_revenue' => 'decimal:2', 'settings_updated_at' => 'datetime', 'subscription_expires_at' => 'datetime', 'grace_period_ends_at' => 'datetime', 'credit_warning_sent_at' => 'datetime', 'credit_warning_threshold' => 'integer', 'max_photos_per_event' => 'integer', 'max_storage_mb' => 'integer', ]; public function events(): HasMany { return $this->hasMany(Event::class); } public function photos(): HasManyThrough { return $this->hasManyThrough( Photo::class, Event::class, 'tenant_id', 'event_id', 'id', 'id' ); } public function members(): HasMany { return $this->hasMany(EventMember::class); } public function purchases(): HasMany { return $this->hasMany(PackagePurchase::class); } public function checkoutSessions(): HasMany { return $this->hasMany(CheckoutSession::class); } public function tenantPackages(): HasMany { return $this->hasMany(TenantPackage::class); } public function announcements(): BelongsToMany { return $this->belongsToMany(TenantAnnouncement::class, 'tenant_announcement_targets') ->withTimestamps(); } public function announcementDeliveries(): HasMany { return $this->hasMany(TenantAnnouncementDelivery::class); } public function retentionOverrides(): HasMany { return $this->hasMany(RetentionOverride::class); } public function packages(): BelongsToMany { return $this->belongsToMany(Package::class, 'tenant_packages') ->withPivot(['price', 'purchased_at', 'expires_at', 'active']) ->withTimestamps(); } public function activeResellerPackage(): HasOne { return $this->hasOne(TenantPackage::class)->where('active', true); } public function notificationLogs(): HasMany { return $this->hasMany(TenantNotificationLog::class); } public function lifecycleEvents(): HasMany { return $this->hasMany(TenantLifecycleEvent::class); } public function canCreateEvent(): bool { return $this->hasEventAllowance(); } public function incrementUsedEvents(int $amount = 1): bool { $package = $this->getActiveResellerPackage(); if (! $package) { return false; } $package->increment('used_events', $amount); return true; } public function setSettingsAttribute($value): void { if (is_string($value)) { $this->attributes['settings'] = $value; return; } $this->attributes['settings'] = json_encode($value ?? []); } public function hasEventAllowance(): bool { $package = $this->getActiveResellerPackage(); if ($package && $package->canCreateEvent()) { return true; } return false; } public function consumeEventAllowance(int $amount = 1, string $reason = 'event.create', ?string $note = null): bool { $package = $this->getActiveResellerPackage(); if ($package && $package->canCreateEvent()) { $previousUsed = (int) $package->used_events; $package->increment('used_events', $amount); $package->refresh(); app(\App\Services\Packages\TenantUsageTracker::class)->recordEventUsage( $package, $previousUsed, $amount ); Log::info('Tenant package usage recorded', [ 'tenant_id' => $this->id, 'tenant_package_id' => $package->id, 'used_events' => $package->used_events, 'amount' => $amount, ]); return true; } Log::warning('Event allowance missing for tenant', [ 'tenant_id' => $this->id, 'reason' => $reason, ]); return false; } public function getActiveResellerPackage(): ?TenantPackage { return $this->activeResellerPackage() ->whereHas('package', fn ($query) => $query->withTrashed()->where('type', 'reseller')) ->where('active', true) ->orderByDesc('expires_at') ->first(); } public function activeSubscription(): Attribute { return Attribute::make( get: fn () => $this->activeResellerPackage()->exists(), ); } public function user(): BelongsTo { return $this->belongsTo(User::class); } public function getStorageQuotaAttribute(): int { $limitMb = (int) ($this->max_storage_mb ?? 0); return max(0, $limitMb) * 1024 * 1024; } public function isInGracePeriod(): bool { if (! $this->grace_period_ends_at) { return false; } return now()->lessThanOrEqualTo($this->grace_period_ends_at); } }