diff --git a/Concerns/HasAnalyticsRelationships.php b/Concerns/HasAnalyticsRelationships.php new file mode 100644 index 0000000..62e95d7 --- /dev/null +++ b/Concerns/HasAnalyticsRelationships.php @@ -0,0 +1,66 @@ +resolveAnalyticsModel('AnalyticsWebsite'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all analytics goals owned by this user. + */ + public function analyticsGoals(): ?HasMany + { + $class = $this->resolveAnalyticsModel('AnalyticsGoal'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Resolve an Analytics model class, checking common namespaces. + */ + protected function resolveAnalyticsModel(string $model): ?string + { + $candidates = [ + "Core\\Mod\\Analytics\\Models\\{$model}", + "App\\Models\\{$model}", + ]; + + foreach ($candidates as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } +} diff --git a/Concerns/HasOrderRelationships.php b/Concerns/HasOrderRelationships.php new file mode 100644 index 0000000..18facde --- /dev/null +++ b/Concerns/HasOrderRelationships.php @@ -0,0 +1,91 @@ +resolveOrderModel('Order'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Check if user can claim a vanity URL. + * + * Requires either: + * - A paid subscription (Creator/Agency package) + * - A one-time vanity URL boost purchase + */ + public function canClaimVanityUrl(): bool + { + // Check for vanity URL boost + $hasBoost = $this->boosts() + ->where('feature_code', 'bio.vanity_url') + ->where('status', Boost::STATUS_ACTIVE) + ->exists(); + + if ($hasBoost) { + return true; + } + + // Check for paid subscription (Creator or Agency package) + $orders = $this->orders(); + + if (! $orders) { + return false; + } + + $hasPaidSubscription = $orders + ->where('status', 'paid') + ->where('total', '>', 0) + ->whereHas('items', function ($query) { + $query->whereIn('item_code', ['creator', 'agency']); + }) + ->exists(); + + return $hasPaidSubscription; + } + + /** + * Resolve an Order model class, checking common namespaces. + */ + protected function resolveOrderModel(string $model): ?string + { + $candidates = [ + "Core\\Mod\\Commerce\\Models\\{$model}", + "Core\\Mod\\Social\\Models\\{$model}", + "App\\Models\\{$model}", + ]; + + foreach ($candidates as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } +} diff --git a/Concerns/HasPageRelationships.php b/Concerns/HasPageRelationships.php new file mode 100644 index 0000000..f20d947 --- /dev/null +++ b/Concerns/HasPageRelationships.php @@ -0,0 +1,237 @@ +resolvePageModel(); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all page projects (folders) owned by this user. + */ + public function pageProjects(): ?HasMany + { + $class = $this->resolvePageProjectModel(); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all custom domains owned by this user. + */ + public function pageDomains(): ?HasMany + { + $class = $this->resolvePageDomainModel(); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all tracking pixels owned by this user. + */ + public function pagePixels(): ?HasMany + { + $class = $this->resolvePagePixelModel(); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get the user's bio.pages entitlement (base + boosts). + */ + public function getBioPagesLimit(): int + { + // Base: 1 page for all tiers + $base = 1; + + // Add from boosts + $boostPages = $this->boosts() + ->where('feature_code', 'bio.pages') + ->where('status', Boost::STATUS_ACTIVE) + ->sum('limit_value'); + + return $base + (int) $boostPages; + } + + /** + * Check if user can create more bio pages. + */ + public function canCreateBioPage(): bool + { + $pages = $this->pages(); + + if (! $pages) { + return false; + } + + return $pages->rootPages()->count() < $this->getBioPagesLimit(); + } + + /** + * Get remaining bio page slots. + */ + public function remainingBioPageSlots(): int + { + $pages = $this->pages(); + + if (! $pages) { + return 0; + } + + return max(0, $this->getBioPagesLimit() - $pages->rootPages()->count()); + } + + /** + * Get the user's sub-page limit (0 base + boosts). + */ + public function getSubPagesLimit(): int + { + // Base: 0 sub-pages (free tier) + $base = 0; + + // Add from boosts + $boostPages = $this->boosts() + ->where('feature_code', 'webpage.sub_pages') + ->where('status', Boost::STATUS_ACTIVE) + ->sum('limit_value'); + + return $base + (int) $boostPages; + } + + /** + * Get the total sub-pages count across all root pages. + */ + public function getSubPagesCount(): int + { + $pages = $this->pages(); + + if (! $pages) { + return 0; + } + + return $pages->subPages()->count(); + } + + /** + * Check if user can create more sub-pages. + */ + public function canCreateSubPage(): bool + { + return $this->getSubPagesCount() < $this->getSubPagesLimit(); + } + + /** + * Get remaining sub-page slots. + */ + public function remainingSubPageSlots(): int + { + return max(0, $this->getSubPagesLimit() - $this->getSubPagesCount()); + } + + /** + * Resolve the Page model class, checking common namespaces. + */ + protected function resolvePageModel(): ?string + { + foreach ($this->getPageModelCandidates('Page') as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } + + /** + * Resolve the Project model class, checking common namespaces. + */ + protected function resolvePageProjectModel(): ?string + { + foreach ($this->getPageModelCandidates('Project') as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } + + /** + * Resolve the Domain model class, checking common namespaces. + */ + protected function resolvePageDomainModel(): ?string + { + foreach ($this->getPageModelCandidates('Domain') as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } + + /** + * Resolve the Pixel model class, checking common namespaces. + */ + protected function resolvePagePixelModel(): ?string + { + foreach ($this->getPageModelCandidates('Pixel') as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } + + /** + * Get candidate class names for a Page module model. + * + * @return list + */ + protected function getPageModelCandidates(string $model): array + { + return [ + "Core\\Mod\\Social\\Models\\{$model}", + "App\\Models\\{$model}", + ]; + } +} diff --git a/Concerns/HasPushRelationships.php b/Concerns/HasPushRelationships.php new file mode 100644 index 0000000..cb205bf --- /dev/null +++ b/Concerns/HasPushRelationships.php @@ -0,0 +1,94 @@ +resolvePushModel('PushWebsite'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all push campaigns owned by this user. + */ + public function pushCampaigns(): ?HasMany + { + $class = $this->resolvePushModel('PushCampaign'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all push segments owned by this user. + */ + public function pushSegments(): ?HasMany + { + $class = $this->resolvePushModel('PushSegment'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all push flows owned by this user. + */ + public function pushFlows(): ?HasMany + { + $class = $this->resolvePushModel('PushFlow'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Resolve a Push model class, checking common namespaces. + */ + protected function resolvePushModel(string $model): ?string + { + $candidates = [ + "Core\\Mod\\Push\\Models\\{$model}", + "App\\Models\\{$model}", + ]; + + foreach ($candidates as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } +} diff --git a/Concerns/HasTrustRelationships.php b/Concerns/HasTrustRelationships.php new file mode 100644 index 0000000..e8bdcd6 --- /dev/null +++ b/Concerns/HasTrustRelationships.php @@ -0,0 +1,66 @@ +resolveTrustModel('TrustCampaign'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Get all trust notifications owned by this user. + */ + public function trustNotifications(): ?HasMany + { + $class = $this->resolveTrustModel('TrustNotification'); + + if (! $class) { + return null; + } + + return $this->hasMany($class); + } + + /** + * Resolve a Trust model class, checking common namespaces. + */ + protected function resolveTrustModel(string $model): ?string + { + $candidates = [ + "Core\\Mod\\Trust\\Models\\{$model}", + "App\\Models\\{$model}", + ]; + + foreach ($candidates as $candidate) { + if (class_exists($candidate)) { + return $candidate; + } + } + + return null; + } +} diff --git a/Models/User.php b/Models/User.php index 85af40f..f344565 100644 --- a/Models/User.php +++ b/Models/User.php @@ -4,6 +4,11 @@ declare(strict_types=1); namespace Core\Tenant\Models; +use Core\Tenant\Concerns\HasAnalyticsRelationships; +use Core\Tenant\Concerns\HasOrderRelationships; +use Core\Tenant\Concerns\HasPageRelationships; +use Core\Tenant\Concerns\HasPushRelationships; +use Core\Tenant\Concerns\HasTrustRelationships; use Core\Tenant\Database\Factories\UserFactory; use Core\Tenant\Enums\UserTier; use Illuminate\Auth\Notifications\VerifyEmail; @@ -20,7 +25,14 @@ use Laravel\Pennant\Concerns\HasFeatures; class User extends Authenticatable implements MustVerifyEmail { - use HasFactory, HasFeatures, Notifiable; + use HasAnalyticsRelationships; + use HasFactory; + use HasFeatures; + use HasOrderRelationships; + use HasPageRelationships; + use HasPushRelationships; + use HasTrustRelationships; + use Notifiable; /** * Create a new factory instance for the model. @@ -74,6 +86,10 @@ class User extends Authenticatable implements MustVerifyEmail ]; } + // ───────────────────────────────────────────────────────────────────────── + // Workspace Relationships + // ───────────────────────────────────────────────────────────────────────── + /** * Get all workspaces this user has access to. */ @@ -103,6 +119,10 @@ class User extends Authenticatable implements MustVerifyEmail ->withTimestamps(); } + // ───────────────────────────────────────────────────────────────────────── + // Tier Helpers + // ───────────────────────────────────────────────────────────────────────── + /** * Get the user's tier. */ @@ -243,6 +263,10 @@ class User extends Authenticatable implements MustVerifyEmail return Namespace_::accessibleBy($this); } + // ───────────────────────────────────────────────────────────────────────── + // Email Verification + // ───────────────────────────────────────────────────────────────────────── + /** * Check if user's email has been verified. * Hades accounts are always considered verified. @@ -283,118 +307,6 @@ class User extends Authenticatable implements MustVerifyEmail return $this->email; } - // ───────────────────────────────────────────────────────────────────────── - // Page Relationships - // ───────────────────────────────────────────────────────────────────────── - - /** - * Get all pages owned by this user. - */ - public function pages(): HasMany - { - return $this->hasMany(Page::class); - } - - /** - * Get all page projects (folders) owned by this user. - */ - public function pageProjects(): HasMany - { - return $this->hasMany(Project::class); - } - - /** - * Get all custom domains owned by this user. - */ - public function pageDomains(): HasMany - { - return $this->hasMany(Domain::class); - } - - /** - * Get all tracking pixels owned by this user. - */ - public function pagePixels(): HasMany - { - return $this->hasMany(Pixel::class); - } - - // ───────────────────────────────────────────────────────────────────────── - // Analytics Relationships - // ───────────────────────────────────────────────────────────────────────── - - /** - * Get all analytics websites owned by this user. - */ - public function analyticsWebsites(): HasMany - { - return $this->hasMany(AnalyticsWebsite::class); - } - - /** - * Get all analytics goals owned by this user. - */ - public function analyticsGoals(): HasMany - { - return $this->hasMany(AnalyticsGoal::class); - } - - // ───────────────────────────────────────────────────────────────────────── - // Push Notification Relationships - // ───────────────────────────────────────────────────────────────────────── - - /** - * Get all push websites owned by this user. - */ - public function pushWebsites(): HasMany - { - return $this->hasMany(PushWebsite::class); - } - - /** - * Get all push campaigns owned by this user. - */ - public function pushCampaigns(): HasMany - { - return $this->hasMany(PushCampaign::class); - } - - /** - * Get all push segments owned by this user. - */ - public function pushSegments(): HasMany - { - return $this->hasMany(PushSegment::class); - } - - /** - * Get all push flows owned by this user. - */ - public function pushFlows(): HasMany - { - return $this->hasMany(PushFlow::class); - } - - // ───────────────────────────────────────────────────────────────────────── - // Trust Widget Relationships - // ───────────────────────────────────────────────────────────────────────── - - /** - * Get all trust campaigns owned by this user. - */ - public function trustCampaigns(): HasMany - { - return $this->hasMany(TrustCampaign::class); - } - - /** - * Get all trust notifications owned by this user. - */ - public function trustNotifications(): HasMany - { - return $this->hasMany(TrustNotification::class); - } - // ───────────────────────────────────────────────────────────────────────── // Entitlement Relationships // ───────────────────────────────────────────────────────────────────────── @@ -407,124 +319,6 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasMany(Boost::class); } - /** - * Get all orders placed by this user. - */ - public function orders(): HasMany - { - return $this->hasMany(Order::class); - } - - /** - * Check if user can claim a vanity URL. - * - * Requires either: - * - A paid subscription (Creator/Agency package) - * - A one-time vanity URL boost purchase - */ - public function canClaimVanityUrl(): bool - { - // Check for vanity URL boost - $hasBoost = $this->boosts() - ->where('feature_code', 'bio.vanity_url') - ->where('status', Boost::STATUS_ACTIVE) - ->exists(); - - if ($hasBoost) { - return true; - } - - // Check for paid subscription (Creator or Agency package) - // An order with total > 0 and status = 'paid' indicates a paid subscription - $hasPaidSubscription = $this->orders() - ->where('status', 'paid') - ->where('total', '>', 0) - ->whereHas('items', function ($query) { - $query->whereIn('item_code', ['creator', 'agency']); - }) - ->exists(); - - return $hasPaidSubscription; - } - - /** - * Get the user's bio.pages entitlement (base + boosts). - */ - public function getBioPagesLimit(): int - { - // Base: 1 page for all tiers - $base = 1; - - // Add from boosts - $boostPages = $this->boosts() - ->where('feature_code', 'bio.pages') - ->where('status', Boost::STATUS_ACTIVE) - ->sum('limit_value'); - - return $base + (int) $boostPages; - } - - /** - * Check if user can create more bio pages. - */ - public function canCreateBioPage(): bool - { - return $this->pages()->rootPages()->count() < $this->getBioPagesLimit(); - } - - /** - * Get remaining bio page slots. - */ - public function remainingBioPageSlots(): int - { - return max(0, $this->getBioPagesLimit() - $this->pages()->rootPages()->count()); - } - - // ───────────────────────────────────────────────────────────────────────── - // Sub-Page Entitlements - // ───────────────────────────────────────────────────────────────────────── - - /** - * Get the user's sub-page limit (0 base + boosts). - */ - public function getSubPagesLimit(): int - { - // Base: 0 sub-pages (free tier) - $base = 0; - - // Add from boosts - $boostPages = $this->boosts() - ->where('feature_code', 'webpage.sub_pages') - ->where('status', Boost::STATUS_ACTIVE) - ->sum('limit_value'); - - return $base + (int) $boostPages; - } - - /** - * Get the total sub-pages count across all root pages. - */ - public function getSubPagesCount(): int - { - return $this->pages()->subPages()->count(); - } - - /** - * Check if user can create more sub-pages. - */ - public function canCreateSubPage(): bool - { - return $this->getSubPagesCount() < $this->getSubPagesLimit(); - } - - /** - * Get remaining sub-page slots. - */ - public function remainingSubPageSlots(): int - { - return max(0, $this->getSubPagesLimit() - $this->getSubPagesCount()); - } - // ───────────────────────────────────────────────────────────────────────── // Referral Relationships // ─────────────────────────────────────────────────────────────────────────