refactor: guard external relationships in User model with class_exists()
Moves all external module relationships (Page, Project, Domain, Pixel, AnalyticsWebsite, AnalyticsGoal, PushWebsite, PushCampaign, PushSegment, PushFlow, TrustCampaign, TrustNotification, Order) out of the User model into dedicated opt-in traits that resolve model classes at runtime via class_exists(). This prevents fatal errors when consuming apps do not have those modules installed. New traits: - HasPageRelationships (pages, projects, domains, pixels, bio/sub-page entitlements) - HasAnalyticsRelationships (analytics websites, goals) - HasPushRelationships (push websites, campaigns, segments, flows) - HasTrustRelationships (trust campaigns, notifications) - HasOrderRelationships (orders, vanity URL entitlement) The User model still uses all five traits so existing consumers see no breaking changes — the relationships now return null instead of throwing a class-not-found error when the backing module is absent. Fixes #6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c51e4310b1
commit
275cba29d7
6 changed files with 579 additions and 231 deletions
66
Concerns/HasAnalyticsRelationships.php
Normal file
66
Concerns/HasAnalyticsRelationships.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Analytics-related relationships for the User model.
|
||||
*
|
||||
* Requires the Analytics module to be installed. All relationships are
|
||||
* guarded with class_exists() so the tenant package remains functional
|
||||
* even when the Analytics module is absent.
|
||||
*
|
||||
* @mixin \Core\Tenant\Models\User
|
||||
*/
|
||||
trait HasAnalyticsRelationships
|
||||
{
|
||||
/**
|
||||
* Get all analytics websites owned by this user.
|
||||
*/
|
||||
public function analyticsWebsites(): ?HasMany
|
||||
{
|
||||
$class = $this->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;
|
||||
}
|
||||
}
|
||||
91
Concerns/HasOrderRelationships.php
Normal file
91
Concerns/HasOrderRelationships.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Concerns;
|
||||
|
||||
use Core\Tenant\Models\Boost;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Order-related relationships for the User model.
|
||||
*
|
||||
* Requires the Order/Commerce module to be installed. All relationships
|
||||
* are guarded with class_exists() so the tenant package remains functional
|
||||
* even when the Commerce module is absent.
|
||||
*
|
||||
* @mixin \Core\Tenant\Models\User
|
||||
*/
|
||||
trait HasOrderRelationships
|
||||
{
|
||||
/**
|
||||
* Get all orders placed by this user.
|
||||
*/
|
||||
public function orders(): ?HasMany
|
||||
{
|
||||
$class = $this->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;
|
||||
}
|
||||
}
|
||||
237
Concerns/HasPageRelationships.php
Normal file
237
Concerns/HasPageRelationships.php
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Concerns;
|
||||
|
||||
use Core\Tenant\Models\Boost;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Page-related relationships for the User model.
|
||||
*
|
||||
* Requires the Page module (Core\Mod\Social or equivalent) to be installed.
|
||||
* All relationships are guarded with class_exists() so the tenant package
|
||||
* remains functional even when the Page module is absent.
|
||||
*
|
||||
* @mixin \Core\Tenant\Models\User
|
||||
*/
|
||||
trait HasPageRelationships
|
||||
{
|
||||
/**
|
||||
* Get all pages owned by this user.
|
||||
*/
|
||||
public function pages(): ?HasMany
|
||||
{
|
||||
$class = $this->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<string>
|
||||
*/
|
||||
protected function getPageModelCandidates(string $model): array
|
||||
{
|
||||
return [
|
||||
"Core\\Mod\\Social\\Models\\{$model}",
|
||||
"App\\Models\\{$model}",
|
||||
];
|
||||
}
|
||||
}
|
||||
94
Concerns/HasPushRelationships.php
Normal file
94
Concerns/HasPushRelationships.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Push notification-related relationships for the User model.
|
||||
*
|
||||
* Requires the Push module to be installed. All relationships are
|
||||
* guarded with class_exists() so the tenant package remains functional
|
||||
* even when the Push module is absent.
|
||||
*
|
||||
* @mixin \Core\Tenant\Models\User
|
||||
*/
|
||||
trait HasPushRelationships
|
||||
{
|
||||
/**
|
||||
* Get all push websites owned by this user.
|
||||
*/
|
||||
public function pushWebsites(): ?HasMany
|
||||
{
|
||||
$class = $this->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;
|
||||
}
|
||||
}
|
||||
66
Concerns/HasTrustRelationships.php
Normal file
66
Concerns/HasTrustRelationships.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Trust widget-related relationships for the User model.
|
||||
*
|
||||
* Requires the Trust module to be installed. All relationships are
|
||||
* guarded with class_exists() so the tenant package remains functional
|
||||
* even when the Trust module is absent.
|
||||
*
|
||||
* @mixin \Core\Tenant\Models\User
|
||||
*/
|
||||
trait HasTrustRelationships
|
||||
{
|
||||
/**
|
||||
* Get all trust campaigns owned by this user.
|
||||
*/
|
||||
public function trustCampaigns(): ?HasMany
|
||||
{
|
||||
$class = $this->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;
|
||||
}
|
||||
}
|
||||
256
Models/User.php
256
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
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue