'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) ->where(function ($query) { $query->whereNull('expires_at')->orWhere('expires_at', '>', now()); }) ->whereHas('package', fn ($query) => $query->withTrashed()->where('type', 'reseller')) ->orderBy('purchased_at') ->orderBy('id'); } 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 hasEventAllowanceFor(?string $includedPackageSlug): bool { $package = $this->getActiveResellerPackageFor($includedPackageSlug); return $package !== null && $package->canCreateEvent(); } 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 consumeEventAllowanceFor(?string $includedPackageSlug, int $amount = 1, string $reason = 'event.create', ?string $note = null): bool { $package = $this->getActiveResellerPackageFor($includedPackageSlug); 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, 'included_package_slug' => $includedPackageSlug, ]); return false; } public function getActiveResellerPackage(): ?TenantPackage { return $this->activeResellerPackage()->with('package')->first(); } public function getActiveResellerPackageFor(?string $includedPackageSlug): ?TenantPackage { $query = $this->tenantPackages() ->with('package') ->where('active', true) ->where(function ($query) { $query->whereNull('expires_at')->orWhere('expires_at', '>', now()); }) ->whereHas('package', fn ($query) => $query->withTrashed()->where('type', 'reseller')) ->orderBy('purchased_at') ->orderBy('id'); if (is_string($includedPackageSlug) && $includedPackageSlug !== '') { $query->whereHas('package', function ($query) use ($includedPackageSlug) { $query->where('included_package_slug', $includedPackageSlug); if ($includedPackageSlug === 'standard') { $query->orWhereNull('included_package_slug'); } }); } return $query->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); } }